Vue3疑问系列(3) — v-show指令是如何工作的?__Vue.js
发布于 3 年前 作者 banyungong 1278 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

根据上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呢

  1. 在看vue3中的vShow指令的实现之前,我们能不能自己写一个vShow呢,毕竟我们都懂指令的工作原理了,那还不好写吗.
  2. 需求: 编写一个指令,绑定在dom和组件上,当绑定的值为真值时就显示,假值的时候就隐藏
  3. 代码如下
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模块

  1. runtime-core: runtime-core 比 runtime-dom 偏底层,很多核心的api都是来自此模块,元素/组件的安装包括diff算法,包括上次讲解的指令内部实现,和内置组件transition keepalive 的底层实现.

  2. runtime-dom: 就是调用runtime-core的api来实现一些好用的指令(v-show|v-model),组件(transition|transition-group),还有元素属性的添加和事件添加及解绑方法,都写在这个模块下

  3. 分这么细的好处我觉得是为了跨平台,不仅限于在浏览器端运行(因为创建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, 以后张三李四想扩展其他模块也很简单.

指令的注册

  1. vue3内置的指令注册是不存在的. what? 不和vue2一样了? 那我不也使用v-show没出问题吗? 你在忽悠我?

  2. 首先确实是没有帮我们全局注册,那为啥我们开发者还能像vue2正常使用呢?

  3. vShow的源码在runtime-dom/src/directives/vShow.ts文件文中,runtime-dom/src/index.ts文件中引入了vShow.ts并且导出了vShow指令对象

  4. vue模块中导出了所有runtime-dom导出方法,也就是我们的大Vue身上有vShow指令对象

  5. 我们使用模板时,编译模块会把我们的模板编译成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]
                ])
            }
        }
    })
  1. 其次我们用户注册指令时,如何指令名和内置的指令名一样,也会报错误提示.

vShow实现

  1. 既然模板上标签使用指令最终会变成 Vue.withDirectives(vnode, [[vShow|vModel|自定义directive, value, argument, modifiers]])

  2. 那我们看看vShow到底怎么实现的?

  3. vShow源码 runtime-dom/src/directives/vShow.ts


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值

  1. transition组件的实现我还没看,后期会单独写两篇文章分别介绍runtime-core下的BaseTransition和runtime-dom下的Transition,这里不做介绍.

  2. 其实明白了指令的内部工作原理,看vShow的实现还是很简单的.

总结

vShow的实现就是在不同时机的钩子中,根据绑定的值,来显示和隐藏el元素

下篇: Vue3疑问系列(4) — v-model(vModelText)指令是如何工作的?

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 徐志伟酱 原文链接:https://juejin.im/post/6933082486096429070

回到顶部