在日常的项目开发中很少会涉及到 Vue 内部的一些知识,只需知道如何使用就就好了;但是在组件开发时却有较大的概率会需要了解 Vue 内部的知识才能开发出强大、灵活、易用的组件,本文将通过举例子介绍两个实用的工具函数。
1. isVueInstance
顾名思义,就是判断一个变量是否是 Vue 组件的实例;我们先来看看使用场景,这里我们以 ant-design-vue
的 dropdown 组件使用代码为例子:
<template>
<a-dropdown>
<a class="ant-dropdown-link" @click="e => e.preventDefault()">
Hover me <a-icon type="down" />
</a>
<a-menu slot="overlay">
<a-menu-item>
<a href="javascript:;">1st menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">2nd menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">3rd menu item</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
不熟悉 ant-design-vue
的小伙伴可以自行点击链接去查看效果。可以看到,这里一共有两个插槽:默认插槽和具名插槽(overlay)。默认插槽是触发 dropdown 显示的元素,具名插槽是 dropdown 上显示的内容。
如果只是上面例子中的代码就没啥好说的了,只需要拿到默认插槽元素给它绑定事件去触发 dropdown 的显示与隐藏就行了,如下(这里是伪代码,意思表达到位即可):
// 这里应该是需要通过 cloneElement 获取 child 的 ref 拿到这是的元素,这里不多赘述,主要讲 isVueInstance
const child = slots.default?.()[0]
if (child) {
child.addEventListener('mouseenter', () => {
dropdownVisible = true
})
child.addEventListener('mouseleave', () => {
dropdownVisible = false
})
}
但要知道,默认插槽既可以是普通的 html 元素,同样也可以是 vue 组件,那么这个时候如果只是简单的对默认插槽进行事件绑定的话肯定会报错,因为 vue 组件上可没有 addEventListener 函数,因此就容易出现报错,这个时候 isVueInstance
函数就起到了大作用了,先来看看如何使用:
const child = slots.default?.()[0]
const el = isVueInstance(child)
? child.$el
: child
if (el) {
el.addEventListener('mouseenter', () => {
dropdownVisible = true
})
el.addEventListener('mouseleave', () => {
dropdownVisible = false
})
}
可以看到,通过 isVueInstance
判断后,不管我们的默认插槽插的是 html 元素还是 vue 组件都能够正确的拿到需要被绑定事件的元素,从而避免报错;那么接下来我们就揭晓一下如何实现 isVueInstance
,其实代码也很简单,如下:
import type { ComponentInstance } from 'vue'
export function isVueInstance(val: any): val is ComponentInstance {
return val && val._isVue && val instance Vue
}
2. isVNode
该方法可以用于判断一个变量是否是 VNode,我们以 element-ui
的 Message 组件为例来看一下:
<template>
<el-button :plain="true" @click="open">打开消息提示</el-button>
<el-button :plain="true" @click="openVn">VNode</el-button>
</template>
<script>
export default {
methods: {
open() {
this.$message('这是一条消息提示');
},
openVn() {
const h = this.$createElement;
this.$message({
message: h('p', null, [
h('span', null, '内容可以是 '),
h('i', { style: 'color: teal' }, 'VNode')
])
});
}
}
}
</script>
可以看到 this.$message
的 message 选项既可以传递字符串也可以传递 VNode,而要实现这个功能就必须识别出 message 选项传递进来的是字符串还是 VNode,为什么这么说呢?这就需要先看看 this.$message
函数是如何实现的
function message(options) {
new Vue({
el: div, // 渲染到一个div上,需要提前创建好,这里不多赘述了
setup() {
return (
<Message
message={options.message}
/>
)
}
})
}
如上代码,实际上 message 函数是调用了 new Vue 将 Message 组件渲染到页面上;上面的代码显然是不支持传递 VNode 的,因为 vue 并不像 react 一样,props 可以传递 ReactNode(与 vue 中的 VNode 类似),因此涉及到如果需要支持传递 VNode 就需要使用插槽了,因此需要区分 message 选项的类型来决定传递到组件的是 props 还是插槽,请看下面代码:
function message(options) {
new Vue({
el: div,
setup() {
return (
<Message
message={
typeof options.message === 'string'
? options.message
: undefined
}
{...{scopedSlots: {
message: isVNode(options.message)
? () => options.message
: undefined
}}}
/>
)
}
})
}
有了 isVNode
函数,就能轻松做出判断,是传递 props 还是插槽了。
那么究竟 isVNode
是如何实现实现的呢?代码如下:
import type { VNode } from 'vue'
function isPlainObject(val: any): val is Record<PropertyKey, unknown> {
return Object.prototype.toString.call(val) === '[object Object]'
}
export function isVNode(val: any): val is VNode {
return val && isPlainObject(val) && Object.hasOwn(val, 'componentOptions')
}
总结
以上就是本文要讲的两个工具函数,它们在组件开发中起了很大的作用,在组件开发中是非常实用的;后续还有继续更新这类工具函数。