前言
根据上2篇文章的讲解,指令的工作原理明白了, 那接下来就是实战篇.
本文的实战不是我们工作的业务场景使用指令实战,而是Vue3中内置的vShow指令的解读.
看看尤大是如何编写vShow指令的.
我们知道指令的作用就是在元素安装、更新、卸载时分别调用不同的钩子,我们开发者在不同的钩子中执行不同的业务逻辑.
vShow指令所要做的功能就是显示和隐藏dom元素(组件也能显示和隐藏)
v-show的使用
const withVShow = (node: VNode, exp: any) =>
withDirectives(node, [[vShow, exp]])
let root: any
beforeEach(() => {
root = document.createElement('div')
})
it('should update show value changed', async () => {
const component = defineComponent({
data() {
return { value: true }
},
render() {
return [withVShow(h('div'), this.value)]
}
})
render(h(component), root)
const $div = root.querySelector('div')
const data = root._vnode.component.data
expect($div.style.display).toEqual('')
data.value = false
await nextTick()
expect($div.style.display).toEqual('none')
data.value = {}
await nextTick()
expect($div.style.display).toEqual('')
data.value = 0
await nextTick()
expect($div.style.display).toEqual('none')
data.value = []
await nextTick()
expect($div.style.display).toEqual('')
data.value = null
await nextTick()
expect($div.style.display).toEqual('none')
data.value = '0'
await nextTick()
expect($div.style.display).toEqual('')
data.value = undefined
await nextTick()
expect($div.style.display).toEqual('none')
data.value = 1
await nextTick()
expect($div.style.display).toEqual('')
})
- 这个单侧比较简单,就是改变component组件实例的value值来显示隐藏div.
- 考虑到部分同学看不懂render函数,下面改成了模板的形式
const component = defineComponent({
data() {
return { value: true }
},
template: `
<div v-show="value"></div>
`
})
- 该单侧就是告诉我们,只要value是真值就显示,value是假值就不显示.
vShow内部实现
自己先写个vShow呢
- 在看vue3中的vShow指令的实现之前,我们能不能自己写一个vShow呢,毕竟我们都懂指令的工作原理了,那还不好写吗.
- 需求: 编写一个指令,绑定在dom和组件上,当绑定的值为真值时就显示,假值的时候就隐藏
- 代码如下
const vShow = (() => {
const setDisplay = (el, value) => {
el.style.display = value
? (el._originalDisplayValue === 'none' ? '' : el._originalDisplayValue)
: 'none'
}
return {
beforeMount(el, binding, vnode, prevVNode) {
el._originalDisplayValue = el.style.display
setDisplay(el, binding.value)
},
updated(el, binding) {
setDisplay(el, binding.value)
},
beforeUnmount(el, binding) {
setDisplay(el, binding.value)
}
}
})()
4.为了验证结果,可以狠狠的点这个链接 https://jsfiddle.net/kpdqtovm/9/ 或者 https://codepen.io/sorrowx/pen/yLVvONp
<iframe height="265" width="600" title="vShow" src="https://codepen.io/sorrowx/embed/preview/yLVvONp?height=265&theme-id=dark&default-tab=js,result"></iframe>vue3内部vShow实现
runtime-dom模块和runtime-core模块
-
runtime-core: runtime-core 比 runtime-dom 偏底层,很多核心的api都是来自此模块,元素/组件的安装包括diff算法,包括上次讲解的指令内部实现,和内置组件transition keepalive 的底层实现.
-
runtime-dom: 就是调用runtime-core的api来实现一些好用的指令(v-show|v-model),组件(transition|transition-group),还有元素属性的添加和事件添加及解绑方法,都写在这个模块下
-
分这么细的好处我觉得是为了跨平台,不仅限于在浏览器端运行(因为创建render函数调用baseCreateRenderer时,可以传入insert, remove, patchProp, forcePatchProp, createElement, createText, createComment, setText, setElementText, parentNode, nextSibling, setScopeId, cloneNode, insertStaticContent这些自定义的方法),runtime-test模块就是最好的证明,大家感兴趣可以看看.这样 runtime-test可以调用runtime-core, runtime-dom也可以调用 runtime-core, 以后张三李四想扩展其他模块也很简单.
指令的注册
-
vue3内置的指令注册是不存在的. what? 不和vue2一样了? 那我不也使用v-show没出问题吗? 你在忽悠我?
-
首先确实是没有帮我们全局注册,那为啥我们开发者还能像vue2正常使用呢?
-
vShow的源码在runtime-dom/src/directives/vShow.ts文件文中,runtime-dom/src/index.ts文件中引入了vShow.ts并且导出了vShow指令对象
-
vue模块中导出了所有runtime-dom导出方法,也就是我们的大Vue身上有vShow指令对象
-
我们使用模板时,编译模块会把我们的模板编译成render函数,render函数中会从Vue身上拿到vShow,然后通过_withDirectives方法来调用(这就回到我们上2篇文章中单测得例子,单测真的很重要,不要一味着使用模板),所以能正常工作
<template>
<div v-show="value">hi</div>
</template>
// 编译成
(function anonymous() {
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return _withDirectives((_openBlock(), _createBlock("div", null, "hi", 512 /* NEED_PATCH */)), [
[_vShow, value]
])
}
}
})
- 其次我们用户注册指令时,如何指令名和内置的指令名一样,也会报错误提示.
vShow实现
-
既然模板上标签使用指令最终会变成 Vue.withDirectives(vnode, [[vShow|vModel|自定义directive, value, argument, modifiers]])
-
那我们看看vShow到底怎么实现的?
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
if (transition && value !== oldValue) {
if (value) {
transition.beforeEnter(el)
setDisplay(el, true)
transition.enter(el)
} else {
transition.leave(el, () => {
setDisplay(el, false)
})
}
} else {
setDisplay(el, value)
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none'
}
-
可以看的出, 尤大的实现还考虑了transition动画组件,抛开transition组件的实现,其实和我们上面写的vShow实现差不多,那就看看呗
-
首先所有钩子,接收4个参数(el:根据vnode创建好的dom元素, binding:参数、修饰符、新的值、旧的值、组件实例, vnode: 当前vnode, prevVNode: 上一次的vnode)
-
beforeMount: 当元素创建好且没有插入到父元素上执行beforeMount钩子,给el添加_vod属性,值为el的display的原始值, 如果有transiton组件且value为真值,则执行transition.beforeEnter(el)否则根据 value值设置el的dispaly.
-
mounted: 当元素安装完后调用beforeMount钩子,如果有transiton组件且value为真值,则执行transition.enter(el)
-
updated: 当元素更新后调用updated钩子,如果有transiton组件且新旧值不同则处理transition进入和离开的逻辑,否则根据value给el设置display值
-
beforeUnmount: 当元素卸载前执行beforeUnmount钩子,根据value给el设置display值
-
transition组件的实现我还没看,后期会单独写两篇文章分别介绍runtime-core下的BaseTransition和runtime-dom下的Transition,这里不做介绍.
-
其实明白了指令的内部工作原理,看vShow的实现还是很简单的.
总结
vShow的实现就是在不同时机的钩子中,根据绑定的值,来显示和隐藏el元素
下篇: Vue3疑问系列(4) — v-model(vModelText)指令是如何工作的?
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 徐志伟酱 原文链接:https://juejin.im/post/6933082486096429070