最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3疑问系列(2) — 在component vnode上绑定指令,指令是如何工作的?

    正文概述 掘金(徐志伟酱)   2021-02-24   3584

    前言

    指令除了可以在普通元素上使用,也可以在组件上使用,作用在组件上,其实本质是subTree vnode 会继承 components vnode的指令dirs, 这样普通subTree vnode有了dirs属性,在普通subTree vnode安装,更新,卸载的时候 便会执行相应的钩子,这在Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?已经解释过了。

    在组件上指令的使用

    要学会看懂单侧,vue3和element-plus都有单侧,单侧我觉得是最好的学习文档。
    下面看下本次用来解释原理的单侧(该单侧代码原地址 )

    it('should work on component vnode', async () => {
        const count = ref(0)
    
        function assertBindings(binding: DirectiveBinding) {
          expect(binding.value).toBe(count.value)
          expect(binding.arg).toBe('foo')
          expect(binding.instance).toBe(_instance && _instance.proxy)
          expect(binding.modifiers && binding.modifiers.ok).toBe(true)
        }
    
        const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          // should not be inserted yet
          expect(el.parentNode).toBe(null)
          expect(root.children.length).toBe(0)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode).toBe(null)
        }) as DirectiveHook)
    
        const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          // should be inserted now
          expect(el.parentNode).toBe(root)
          expect(root.children[0]).toBe(el)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode).toBe(null)
        }) as DirectiveHook)
    
        const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          expect(el.parentNode).toBe(root)
          expect(root.children[0]).toBe(el)
    
          // node should not have been updated yet
          // expect(el.children[0].text).toBe(`${count.value - 1}`)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode!.type).toBe(_prevVnode!.type)
        }) as DirectiveHook)
    
        const updated = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          expect(el.parentNode).toBe(root)
          expect(root.children[0]).toBe(el)
    
          // node should have been updated
          expect(el.children[0].text).toBe(`${count.value}`)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode!.type).toBe(_prevVnode!.type)
        }) as DirectiveHook)
    
        const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          // should be removed now
          expect(el.parentNode).toBe(root)
          expect(root.children[0]).toBe(el)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode).toBe(null)
        }) as DirectiveHook)
    
        const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
          expect(el.tag).toBe('div')
          // should have been removed
          expect(el.parentNode).toBe(null)
          expect(root.children.length).toBe(0)
    
          assertBindings(binding)
    
          expect(vnode.type).toBe(_vnode!.type)
          expect(prevVNode).toBe(null)
        }) as DirectiveHook)
    
        const dir = {
          beforeMount,
          mounted,
          beforeUpdate,
          updated,
          beforeUnmount,
          unmounted
        }
    
        let _instance: ComponentInternalInstance | null = null
        let _vnode: VNode | null = null
        let _prevVnode: VNode | null = null
    
        const Child = (props: { count: number }) => {
          _prevVnode = _vnode
          _vnode = h('div', props.count)
          return _vnode
        }
    
        const Comp = {
          setup() {
            _instance = currentInstance
          },
          render() {
            return withDirectives(h(Child, { count: count.value }), [
              [
                dir,
                // value
                count.value,
                // argument
                'foo',
                // modifiers
                { ok: true }
              ]
            ])
          }
        }
    
        const root = nodeOps.createElement('div')
        render(h(Comp), root)
    
        expect(beforeMount).toHaveBeenCalledTimes(1)
        expect(mounted).toHaveBeenCalledTimes(1)
    
        count.value++
        await nextTick()
        expect(beforeUpdate).toHaveBeenCalledTimes(1)
        expect(updated).toHaveBeenCalledTimes(1)
    
        render(null, root)
        expect(beforeUnmount).toHaveBeenCalledTimes(1)
        expect(unmounted).toHaveBeenCalledTimes(1)
     })
    

    该单侧和 '在普通vnode上绑定指令,指令是如何工作的?'一文中的单侧差不多。
    单侧意图告诉我们:

    1. render(h(Comp), root) 安装组件时,会分别执行 beforeMount mounted钩子函数
    2. count.value++ 组件更新时,会分别执行 beforeUpdate updated钩子函数
    3. render(null, root) 组件卸载时 会分别执行 beforeUnmount unmounted钩子函数

    本篇文章的目的就是要探索在源码内部何时执行这些钩子的,知道了内部实现,以后使用指令时会更加得心应手.

    考虑到部分同学,对render函数比较陌生,我也对该单侧进行了 template 的改写,这样就能直观地看懂该单侧了.

    <script>
        import { ref, render, h } from 'vue'
        const count = ref(0)
        const root = document.getElementById('app')
    
        const Child = {
            props: {
                type: Number,
                default: 0
            },
            template: `
                <div>{{ count }}</div>
            `
        }
    
        const Comp = {
            directives: {
                xxx: {
                    beforeMount,
                    mounted,
                    beforeUpdate,
                    updated,
                    beforeUnmount,
                    unmounted
                }
            },
            template: `
                <Child v-xxx:foo.ok="count" />
            `
        }
    
        render(h(Comp), root)
    </script>
    

    这么改写完,就没有理由看不懂了吧.
    嗯,那就进入正题,看看源码内部究竟何时调用这些钩子呢?

    内部实现

    初始化

        render(h(Comp), root)
    
    1. render函数 -> patch函数 -> processComponent函数[处理Comp组件] -> mountComponent函数[安装Comp组件] -> instance.update函数【拿到subTree Child vnode节点 -> patch函数 -> processComponent函数[处理Child组件] -> mountComponent函数[安装Child组件] -> instance.update函数[拿到subTree div vnode节点,此时div vnode继承了 Child vnode的dirs] -> 安装完subTree div vnode节点[subTree div vnode el 赋值给 subTree Child vnode el上]] -> 安装完subTree Child vnode节点[subTree Child vnode el赋值给comp vnode el] ] 】-> Comp vnode安装完[comp vnode el挂载到root下]
    • 组件vnode节点都会安装他的subTree vnode
    • 普通元素vnode children属性可能有值,有值都会递归地安装他的孩子vnode
    • 所以上面调用栈就很长了
    • 关于组件如何patch到浏览器,后面会单独写一篇文章介绍[从模板 -> render -> vnode -> diff -> 真实dom[el创建、属性、事件如何添加到el上]]
    1. 这里重点说下获取组件下subTree的方法,该方法内部会使subTree vnode 继承 组件vnode 的dirs
    export function renderComponentRoot(
      instance: ComponentInternalInstance
    ): VNode {
      const {
        type: Component,
        vnode,
        proxy,
        withProxy,
        props,
        propsOptions: [propsOptions],
        slots,
        attrs,
        emit,
        render,
        renderCache,
        data,
        setupState,
        ctx
      } = instance
    
      let result
      currentRenderingInstance = instance
      if (__DEV__) {
        accessedAttrs = false
      }
      try {
        let fallthroughAttrs
        if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          // withProxy is a proxy with a different `has` trap only for
          // runtime-compiled render functions using `with` block.
          const proxyToUse = withProxy || proxy
          result = normalizeVNode(
            render!.call(
              proxyToUse,
              proxyToUse!,
              renderCache,
              props,
              setupState,
              data,
              ctx
            )
          )
          fallthroughAttrs = attrs
        } else {
          // functional
          const render = Component as FunctionalComponent
          // in dev, mark attrs accessed if optional props (attrs === props)
          if (__DEV__ && attrs === props) {
            markAttrsAccessed()
          }
          result = normalizeVNode(
            render.length > 1
              ? render(
                  props,
                  __DEV__
                    ? {
                        get attrs() {
                          markAttrsAccessed()
                          return attrs
                        },
                        slots,
                        emit
                      }
                    : { attrs, slots, emit }
                )
              : render(props, null as any /* we know it doesn't need it */)
          )
          fallthroughAttrs = Component.props
            ? attrs
            : getFunctionalFallthrough(attrs)
        }
    
        // attr merging
        // in dev mode, comments are preserved, and it's possible for a template
        // to have comments along side the root element which makes it a fragment
        let root = result
        let setRoot: ((root: VNode) => void) | undefined = undefined
        if (
          __DEV__ &&
          result.patchFlag > 0 &&
          result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
        ) {
          ;[root, setRoot] = getChildRoot(result)
        }
    
        if (Component.inheritAttrs !== false && fallthroughAttrs) {
          const keys = Object.keys(fallthroughAttrs)
          const { shapeFlag } = root
          if (keys.length) {
            if (
              shapeFlag & ShapeFlags.ELEMENT ||
              shapeFlag & ShapeFlags.COMPONENT
            ) {
              if (propsOptions && keys.some(isModelListener)) {
                // If a v-model listener (onUpdate:xxx) has a corresponding declared
                // prop, it indicates this component expects to handle v-model and
                // it should not fallthrough.
                // related: #1543, #1643, #1989
                fallthroughAttrs = filterModelListeners(
                  fallthroughAttrs,
                  propsOptions
                )
              }
              root = cloneVNode(root, fallthroughAttrs)
            } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
              const allAttrs = Object.keys(attrs)
              const eventAttrs: string[] = []
              const extraAttrs: string[] = []
              for (let i = 0, l = allAttrs.length; i < l; i++) {
                const key = allAttrs[i]
                if (isOn(key)) {
                  // ignore v-model handlers when they fail to fallthrough
                  if (!isModelListener(key)) {
                    // remove `on`, lowercase first letter to reflect event casing
                    // accurately
                    eventAttrs.push(key[2].toLowerCase() + key.slice(3))
                  }
                } else {
                  extraAttrs.push(key)
                }
              }
              if (extraAttrs.length) {
                warn(
                  `Extraneous non-props attributes (` +
                    `${extraAttrs.join(', ')}) ` +
                    `were passed to component but could not be automatically inherited ` +
                    `because component renders fragment or text root nodes.`
                )
              }
              if (eventAttrs.length) {
                warn(
                  `Extraneous non-emits event listeners (` +
                    `${eventAttrs.join(', ')}) ` +
                    `were passed to component but could not be automatically inherited ` +
                    `because component renders fragment or text root nodes. ` +
                    `If the listener is intended to be a component custom event listener only, ` +
                    `declare it using the "emits" option.`
                )
              }
            }
          }
        }
    
        // inherit directives
        if (vnode.dirs) {
          if (__DEV__ && !isElementRoot(root)) {
            warn(
              `Runtime directive used on component with non-element root node. ` +
                `The directives will not function as intended.`
            )
          }
          root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
        }
        // inherit transition data
        if (vnode.transition) {
          if (__DEV__ && !isElementRoot(root)) {
            warn(
              `Component inside <Transition> renders non-element root node ` +
                `that cannot be animated.`
            )
          }
          root.transition = vnode.transition
        }
    
        if (__DEV__ && setRoot) {
          setRoot(root)
        } else {
          result = root
        }
      } catch (err) {
        handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
        result = createVNode(Comment)
      }
      currentRenderingInstance = null
    
      return result
    }
    
    const subTree = (instance.subTree = renderComponentRoot(instance))
    

    该函数很长,但是我们也没有必要全部看完,只要知道该函数的传参是当前组件的实例,返回值是组件render函数返回的vnode即可

        // inherit directives
        if (vnode.dirs) {
          if (__DEV__ && !isElementRoot(root)) {
            warn(
              `Runtime directive used on component with non-element root node. ` +
                `The directives will not function as intended.`
            )
          }
          root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
        }
    

    对于本单侧,child组件的vnode是有dirs,但是他模板的div vnode是没有dirs值的
    上面这段代码就是把child vnode 的dirs给了 div vnode dirs

    3.普通 subTree vnode继承了组件vnode的dirs值,那普通 subTree vnode安装、更新、卸载时,何时调用那些钩子我就不重复讲解了。
    不了解的可以查看Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?

    更新

    • 更新时执行 instance.update内部会调用 const nextTree = renderComponentRoot(instance)
    • nextTree vnode也继承了 component vnode dirs值
    • 新老 nextTree vnode 节点 和 prevTree vnode 进行 patch
    • 最终还是回到 普通vnode 的diff,可以参考Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?中的组件更新

    卸载

    可以参考Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?中的组件卸载

    总结

    指令用在普通vnode和组件vnode上,是如何工作的,都讲完了.
    组件vnode上指令工作的原理就是subTree vnode 继承了component vnode dirs的值,其他就和普通vnodes上指令工作的原理一样了.

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


    起源地下载网 » Vue3疑问系列(2) — 在component vnode上绑定指令,指令是如何工作的?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元