【vue】什么是vnode hook?

2,405 阅读3分钟

1、什么是vnode hook?

在对新旧vnode进行patch的过程中,在不同的阶段提供的一些访问vnode的钩子。vnode hook贯穿整个patch的过程, 像dom元素的属性、事件、class、style的更新,ref, 指令,transition都是通过vnode hook来实现的。patch方法是处理diff的整体流程逻辑的,而hook就是用来干活的。

2、组件vnode的hook。

  1. init:创建内部组件时调用 image.png 在init内部会根据vnode创建组件实例:
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      // 如果是使用了keepAlive的组件,则会复用上一次的组件实例
      const mountedNode: any = vnode // work around flow
      // 并且进行prepatch
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 创建组件实例
      // 在组件的vnode init的时候会创建对应的组件实例
      // init执行完也代表组件已经创建成功,dom已经在内存中创建出来了
      // 真正执行mounted钩子是在根节点的dom元素生成后, 通过insertedVnodeQueue来批量执行的
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode, // VNode节点对象
        activeInstance // 父组件实例
      )
      // 挂载组件
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  1. insert: 当整个vnode树都patch完成才会执行,通过维持insertedVnodeQueue来保证所有组件的mouted顺序执行。
  function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    // 只有根组件的mounted事件是在自己patch完成后执行的
    // 内部组件不是这样的,虽然内部组件patch完成后dom元素已经生成了并且插入到了父节点中
    // 但是mounted生命周期是延迟执行的。
    if (isTrue(initial) && isDef(vnode.parent)) {
      // 将当前vnode的插入队列放到父组件的pendingInsert上
      vnode.parent.data.pendingInsert = queue
    } else {
      // 遍历vnode队列,执行vonde.data.hook.insert
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
  }

执行mounted

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    // 在子组件调用$mount成功后并不会触发 mounted 钩子, 只有根组件才会调用
    // 子组件的mounted是在这里触发的
    // 这样可以保证在任何一个子组件的mounted里通过refs访问任何组件的实例都能有结果
    // 假设是在$mount里调用mounted,此时子组件的下一个兄弟组件还未创建成功
    // 这样的话是访问不到下一个兄弟组件的
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
  1. prepatch:对组件patch之前调用
function patchVnode() {
    // ....
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    // ....
}

prepatch的逻辑:

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 当vnode进行patch时会对上一次创建出来的组件实例进行复用
    // 然后对组件的属性、事件、子节点进行更新
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    debugger
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
  1. destroy:vnode被销毁时调用
  function invokeDestroyHook (vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      // 先调用用户定义的destroy hook, 再调用vue定义的destroy hook
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    // 如果存在children, 则递归调用
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }

destroy的逻辑:

destroy (vnode: MountedComponentVNode) {
    // 当vnode节点销毁时对应的组件实例也会被销毁
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      // 如果不是keep-alive组件, 直接销毁组件
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        // keep-alive组件
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
}

3、普通的vnode的hook

  1. create:vnode对应的dom元素创建完成时调用。
    • updateAttrs:更新dom元素的普通属性
    • updateClass:更新dom元素的class
    • updateDOMListeners:更新dom元素的事件
    • updateDOMProps:更新dom元素的innerHTML、textContent等特殊属性。
    • updateStyle:更新dom元素的style
    • _enter:transition组件使用,动画逻辑是在这里定义的
    • create:registerRef注册ref
    • updateDirectives:更新指令
  2. update
    • updateAttrs(oldVnode, vnode)
    • updateClass(oldVnode, vnode)
    • updateDOMListeners(oldVnode, vnode)
    • updateDOMProps(oldVnode, vnode)
    • updateStyle(oldVnode, vnode)
    • update(oldVnode, vnode)
    • updateDirectives(oldVnode, vnode)
  3. remove: dom元素从页面中移除前调用,transition会使用到。
  4. destroy
    • destroy(vnode): 销毁ref
    • unbindDirectives(vnode):指令unbind
  5. active: keep-alive组件重新展示时调用,transition会使用到。

4、vnode hook 测试代码

有兴趣的朋友可以跑一下下面的代码,可能会对patch的过程理解有帮助。我这篇文章可能是从不同的角度去看patch,已经有点钻牛角尖了, 如果对vue的pacth过程感兴趣的朋友, 刚开始千万不要迷失在这些hook里面。知道大概是做什么的就好, 还是得从整体流程去理解。比如指令、ref、dom的等建议不要和patch的整体逻辑一起研究,单独拿出来研究即可。

      const Comp = {
        data() {
          return {
            msg: 1,
          };
        },
        props: {
          msg1: String,
        },
        methods: {
          handleClick() {
            this.msg = Math.random();
          },
        },
        render(h) {
          let vm = this;
          return h(
            "div",
            {
              hook: {
                // 创建的时候调用的hook
                create(oldVnode, vnode) {
                  //  这个时候已经根据vnode创建出dom节点了, 但是还未插入到父节点中去
                  console.log("create", oldVnode, vnode);
                  vnode.elm.style.cssText = "color: red";
                },
                insert(oldVnode, vnode) {
                  console.log("insert", oldVnode, vnode);
                },
                // 更新时调用的 hook
                prepatch(oldVnode, vnode) {
                  console.log("prepatch", oldVnode, vnode);
                },
                update(oldVnode, vnode) {
                  console.log("update", oldVnode, vnode);
                },
                postpatch(oldVnode, vnode) {
                  console.log("postpatch", oldVnode, vnode);
                },
                destroy(oldVnode, vnode) {
                  console.log("destroy", oldVnode, vnode);
                },
              },
              on: {
                click: this.handleClick,
              },
            },
            [
              h(
                "button",
                {
                  on: {
                    click: this.handleClick,
                  },
                },
                "组件内部修改状态"
              ),
              this.msg + "  " + this.msg1,
            ]
          );
        },
      };
      new Vue({
        el: "#app",
        components: { Comp },
        data() {
          return {
            showComp: true,
            msg1: "a",
          };
        },
        methods: {
          handleToggle() {
            this.showComp = !this.showComp;
          },
          changeMsg1() {
            this.msg1 = Math.random() + "";
          },
        },
        render(h) {
          return h("div", [
            h(
              "button",
              {
                on: {
                  click: this.handleToggle,
                },
              },
              this.showComp ? "隐藏" : "显示"
            ),
            h(
              "button",
              {
                on: {
                  click: this.changeMsg1,
                },
              },
              "给子组件发送消息"
            ),
            this.showComp
              ? h("Comp", {
                  props: {
                    msg1: this.msg1,
                  },
                  hook: {
                    init(vnode, hydrating) {
                      console.log("组件Comp init", vnode, hydrating);
                    },
                    insert(vnode) {
                      console.log("组件Comp insert", vnode);
                    },
                    prepatch(oldVnode, vnode) {
                      console.log("组件Comp prepatch", oldVnode, vnode);
                    },
                    postpatch(oldVnode, vnode) {
                      console.log("组件Comp postpatch", oldVnode, vnode);
                    },
                    destroy(vnode) {
                      console.log("组件Comp destroy", vnode);
                    },
                  },
                })
              : null,
          ]);
        },
      });