Skip to content
On this page

函数式调用 vue 组件

在我们使用组件库的时候,比如 Element,我们可以看到有些组件是可以使用函数调用的方式来使用的。

比如,Message 组件中,可以直接使用 this.$message('这是一条消息提示'),或者需要响应用户操作的组件 MessageBox,可以返回一个 Promise

js
this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
  confirmButtonText: "确定",
  cancelButtonText: "取消",
  type: "warning",
})
  .then(() => {
    this.$message({
      type: "success",
      message: "删除成功!",
    })
  })
  .catch(() => {
    this.$message({
      type: "info",
      message: "已取消删除",
    })
  })

实现函数调用

Element 是将常用的函数挂载在了 Vue.prototype 上,所以能够直接使用 this.$message 来调用,这里我们实现的时候就略过这一步了,选择在使用时引入需要的函数。

需要涉及的 API 有 extend 和 $mount、$destroy

实现原理是 extend 能够根据提供的对象(包含组件选项)创建一个 Vue 的子类,然后再通过 $mount 和 $destroy 控制组件的挂载与销毁

我们先来实现一个简易的 Message 组件

我们先写好 Message.vue 文件,直接写就行了

vue
<template>
  <div class="msg" :style="border">
    {{ message }}
  </div>
</template>

<script>
export default {
  name: "Message",
  data() {
    return {
      message: "default",
      type: "info",
      typeMap: {
        success: "green",
        info: "gray",
        warning: "orange",
        error: "red",
      },
    }
  },
  computed: {
    border() {
      return `border-color: ${this.typeMap[this.type]}`
    },
  },
}
</script>

<style scoped>
.msg {
  height: 40px;
  line-height: 40px;
  border-width: 1px;
  border-style: solid;
  margin-bottom: 10px;
}
</style>

既然要函数式调用,那就需要暴露出去一个可以使用这个组件的方法,我们新建一个 index.js 文件

js
import Vue from "vue"
import Main from "./Message"

let MessageConstructor = Vue.extend(Main)

const Message = options => {
  let instance = new MessageConstructor({
    data: options,
  })

  instance.$mount()
  document.body.appendChild(instance.$el)
}

export default Message

在其他组件中,引入 Message 方法就可以直接使用了

js
Message({
  message: "道生一,一生二,二生三,三生万物",
  type: "success",
})

是不是很简单,来回顾下 index.js 做了什么,

在第四行中,我们使用 extend 基于 Message.vue 组件创建一个新的类,这个类如果生成实例的话,那就是初始化的 Message 组件,这当然不能满足需求啦,常规组件还能传递 props 呢。所以我们在使用新的构造函数时,可以传递 data,传递的 data 与组件内的 data 属性会进行合并,传递的 data 中属性值会将原先的 data 属性值覆盖

随后在第 11 行,我们手动将组件 mount,渲染之后的实例就是一个完全的 Vue 实例了,与没有 mount 的组件的区别就是能够访问到 $el,即组件的根节点

然后在 12 行,我们将组件的根节点挂载到 body 上,当然除了 body,其他节点也是可以的,只需在调用 Message 是传递挂载到的 DOM 即可。

使用 $mount 时,还可以直接传递需要挂载到的节点,就可以省去手动插入文档这一步

js
instance.$mount("#app") // 会替换掉 #app

instance.$mount({ el: "#app" }) // 同上

如果想要实现在组件外部手动销毁 Message 组件,则需要在 Message.vue 中添加方法

js
destroy(){
  this.$destroy()
  this.$el.parentNode.removeChild(this.$el)
}

需要更改一下 Message 方法,返回 Vue 实例

js
const Message = options => {
  let instance = new MessageConstructor({
    data: options,
  })

  instance.$mount()
  document.body.appendChild(instance.$el)
  return instance
}

在需要调用这个方法的组件中,保留 Message 方法返回的实例,需要销毁的时候,调用组件内部自定义的 destroy 方法,即可实现组件的销毁

vue
<template>
  <div class="app" id="app">
    <button @click="alert">弹出</button>
    <button @click="destroy">销毁</button>
  </div>
</template>

<script>
import Message from "@/components/Message"
export default {
  name: "App",
  data() {
    return {
      count: 1,
      instance: null,
    }
  },
  methods: {
    alert() {
      this.instance = Message({
        message: `${this.count}`,
        type: "success",
      })
      this.count++
    },
    destroy() {
      this.instance.destroy()
    },
  },
}
</script>

调用函数返回 Promise

上面的 Message 组件只是简单的控制了组件的渲染、销毁,那如果组件再复杂一点,有自己的内部 data 需要向外传递怎么办呢,例如 Element 中的 MessageBox 组件,允许用户在组件内部输入,并通过 Promise 将用户操作的传递出来

还是一样,先写好 MessageBox.vue

vue
<template>
  <div class="msgBox">
    <input type="text" v-model="text" />
    <button @click="confirm">confirm</button>
    <button @click="cancel">cancel</button>
  </div>
</template>

<script>
export default {
  name: "MessageBox",
  data() {
    return {
      text: "",
    }
  },
  methods: {
    closeMsg() {
      this.$destroy()
      this.$el.parentNode.removeChild(this.$el)
    },
  },
}
</script>

要想返回一个 Promise,那肯定得在定义函数时做点文章,先还是熟悉那一套,在 return 的部分我们做了一点改动

js
import Vue from "vue"
import Main from "./MessageBox"

let MessageConstructor = Vue.extend(Main)

const MessageBox = options => {
  let instance = new MessageConstructor({
    data: options,
  })
  instance.$mount()
  document.body.appendChild(instance.$el)
  return new Promise((resolve, reject) => {
    if (instance.state === "fulfilled") {
      resolve(instance.text)
    }
    if (instance.state === "rejected") {
      reject("点了取消")
    }
  })
}

export default MessageBox

我们预想的是点击组件内部的两个按钮,分别将组件内部的 state 更改为 fulfilled 或者 rejected 状态,但是并没有生效,这是为啥呢

执行 Promise 的时候,Promise 主体是同步执行的,也就是说,并不会在实例的 state 更改之后,再执行 resolve 或者 reject,执行到这部分的时候,只会判断组件的初始 state

那咋办,那就只好在组件内部控制函数中 Promise 了,我们将一个函数传递给组件,这个函数可以 resolve 或者 reject,在这之前,得先把 Promise 内部的 resolve、reject 保留下来

js
import Vue from "vue"
import Main from "./MessageBox"

let MessageConstructor = Vue.extend(Main)

let instance
let asyncHandler

const defaultCallback = action => {
  if (action === "confirm") {
    asyncHandler.resolve(instance.text)
  }
  if (action === "cancel") {
    asyncHandler.reject()
  }
}

const MessageBox = options => {
  instance = new MessageConstructor({
    data: options,
  })
  instance.callback = defaultCallback
  instance.$mount()
  document.body.appendChild(instance.$el)
  return new Promise((resolve, reject) => {
    asyncHandler = {
      resolve,
      reject,
    }
  })
}

export default MessageBox

在组件中,我们新增两个方法 confirm 和 cancel

vue
<template>
  <div class="msgBox">
    <input type="text" v-model="text" />
    <button @click="confirm">confirm</button>
    <button @click="cancel">cancel</button>
  </div>
</template>

<script>
export default {
  name: "MessageBox",
  data() {
    return {
      message: "",
      text: "",
      callback: null,
    }
  },
  methods: {
    closeMsg() {
      this.$destroy()
      this.$el.parentNode.removeChild(this.$el)
    },
    confirm() {
      this.callback("confirm")
    },
    cancel() {
      this.callback("cancel")
    },
  },
}
</script>

在使用时,我们就可以直接

js
MessageBox()
  .then(res => {
    console.log(res)
  })
  .catch(() => {
    console.log("点了取消")
  })

调用 MessageBox 方法之后会弹出 MessageBox 组件,在组件内输入框内部填写字符串,再点击 confirm 就会将填写的字符串打印出来了。

你可能会注意到,再次点击 confirm 为什么就不再打印了呢。那是因为 Promise 的 then、catch 方法只会在 Promise 更改状态为 fulfilled 或者 rejected 之后执行一次啊,小傻瓜

OK,大功告成 🎉

参考链接