Vue创建真实节点以及添加到父节点时机分析

643 阅读4分钟

首先,需要明确,原生节点和组件节点被添加的时机是不一样的,虽然都是在Vue中调用了insert方法,但是调用insert的时机完全不一样

其次,我们需要关注VNode上的elm属性,这个属性代表了VNode对应的真实节点,这个属性只有被赋值了,才可能insert到parent上去

再其次,Vue依次创建父子组件时,真实节点是如何被挨个添加到父元素上,这个过程触发了哪些hook,这个触发的顺序也是值得研究的

原生节点

对于原生节点来说,vnode上的elm属性添加的时机是

在new Watcher() -> watcher.get() -> vm.updateComponent() -> vm.render -> vm.update -> patch -> createElm里面

举例来说:

<div id="app">{{abc}}</div>
const vm = new Vue({
    el: '#app',
    data: {
        abc: '123'
    },
});

该案例的render方法为:

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(abc))])}
})

接下来调用_c走到_createElement里面后,其创建的VNode节点elm属性为undefined 在patch里面,可以看到调用createElm时需要的parentElm、refElm两个参数是通过oldVnode.elm定位得到的:

// oldVnode此时是真实节点#app
oldVnode = emptyNodeAt(oldVnode);
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm)

createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    nodeOps.nextSibling(oldElm)
);

在此处parentElm就是body,nodeOps.nextSibling(oldElm)是一个文本节点

    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      if (isDef(vnode.elm) && isDef(ownerArray)) {
        // This vnode was used in a previous render!
        // now it's used as a new node, overwriting its elm would cause
        // potential patch errors down the road when it's used as an insertion
        // reference node. Instead, we clone the node on-demand before creating
        // associated DOM element for it.
        vnode = ownerArray[index] = cloneVNode(vnode);
      }

      vnode.isRootInsert = !nested; // for transition enter check
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }

      var data = vnode.data;
      var children = vnode.children;
      var tag = vnode.tag;
      if (isDef(tag)) {
        {
          if (data && data.pre) {
            creatingElmInVPre++;
          }
          if (isUnknownElement(vnode, creatingElmInVPre)) {
            warn(
              'Unknown custom element: <' + tag + '> - did you ' +
              'register the component correctly? For recursive components, ' +
              'make sure to provide the "name" option.',
              vnode.context
            );
          }
        }

        vnode.elm = vnode.ns
          ? nodeOps.createElementNS(vnode.ns, tag)
          : nodeOps.createElement(tag, vnode);

在createElm方法中,我们可以看到根据vnode创建出来的真实节点会赋值给vnode.elm 接下来在创建完children后,就把vnode.elm插入到了parentElm中去

    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
        // ...
        vnode.elm = vnode.ns
          ? nodeOps.createElementNS(vnode.ns, tag)
          : nodeOps.createElement(tag, vnode);
        setScope(vnode);

        /* istanbul ignore if */
        {
          createChildren(vnode, children, insertedVnodeQueue);
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue);
          }
          insert(parentElm, vnode.elm, refElm);
        }

再顺便说一下createChildren的过程:

    function createChildren (vnode, children, insertedVnodeQueue) {
      if (Array.isArray(children)) {
        {
          checkDuplicateKeys(children);
        }
        for (var i = 0; i < children.length; ++i) {
          createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
        }
      } else if (isPrimitive(vnode.text)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
      }
    }

由于执行到createChildren(vnode, children, insertedVnodeQueue);时,vnode上已经有elm属性了,所以在createChildren里面再调用createElm时,就很自然地以vnode.elm作为parent

组件节点

原生节点创建并插入的流程就分析到这里,接下来我们看组件节点,我们以下面的案例来说明:

    <div id="app">
        <my :data="abc"></my>
    </div>
    <script src="../dist/vue.js"></script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                abc: '123' // 属性是在父组件中定义的传递给了子组件 (父组件的定义的数据 已经是响应式的了)
            },
            components: {
                my: {
                    props: {
                        data: {type:String}
                    },
                    template: '<div>my-component {{data}}</div>'
                }
            }
        });
    </script>

沿着new Watcher() -> watcher.get() -> vm.updateComponent() -> vm.render -> vm.update -> patch -> createElm的流程,又走到了createElm里面,但是走到createComponent的判断之后,就到进入该分支中了:

    function createElm (
      vnode,
      insertedVnodeQueue,
      parentElm,
      refElm,
      nested,
      ownerArray,
      index
    ) {
      if (isDef(vnode.elm) && isDef(ownerArray)) {
        // This vnode was used in a previous render!
        // now it's used as a new node, overwriting its elm would cause
        // potential patch errors down the road when it's used as an insertion
        // reference node. Instead, we clone the node on-demand before creating
        // associated DOM element for it.
        vnode = ownerArray[index] = cloneVNode(vnode);
      }

      vnode.isRootInsert = !nested; // for transition enter check
      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }

注意,此时还没有到给vnode.elm赋值的时机,所以可以猜测,给vnode.elm赋值为真实dom节点,以及insert方法的执行,都在createComponent里面:

    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */); // init 走完以后 
        }
        // after calling the init hook, if the vnode is a child component
        // it should've created a child instance and mounted it. the child
        // component also has set the placeholder vnode's elm.
        // in that case we can just return the element and be done.
        if (isDef(vnode.componentInstance)) { // 因为实例复用了 实例上就有老的属性 
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm); // 直接将缓存的dom元素插入
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }

可以看到如果命中if (isDef(vnode.componentInstance)) 这个条件,就会执行insert(parentElm, vnode.elm, refElm),现在问题来了: 1、什么时候能命中这个if判断 2、vnode.elm是怎么被赋值的

对于第一个问题,我们可以看到在该if判断的上方执行了vnode.data.hook.init方法,可以猜测,在这个初始化方法内部给vnode加了componentInstance属性,我们不妨打开init方法看一下:

  var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch

        // 如果缓存过 则直接走 prepatch 不走初始化流程了

        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance // 这个是父组件的实例
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },

可以看到,确实如我们预想的一样,在vnode上加了componentInstance这个属性,其值指向组件实例,第1个问题就解决了 第2个问题,在createComponent中,进入initComponent后,可以看到,给vnode.elm赋了值:

    function initComponent (vnode, insertedVnodeQueue) {
      if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
        vnode.data.pendingInsert = null;
      }
      vnode.elm = vnode.componentInstance.$el;
      if (isPatchable(vnode)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
        setScope(vnode);
      } else {
        // empty component root.
        // skip all element-related modules except for ref (#3455)
        registerRef(vnode);
        // make sure to invoke the insert hook
        insertedVnodeQueue.push(vnode);
      }
    }

这个值是从vnode.componentInstance.el上取到的,所以我们有必要跟踪一下vnode.componentInstance.el上取到的,所以我们有必要跟踪一下vnode.componentInstance.el这个值,看看它又从哪里来,我们先来进入createComponentInstanceForVnode看一下它的创建过程:

  function createComponentInstanceForVnode (
    // we know it's MountedComponentVNode but flow doesn't
    vnode,
    // activeInstance in lifecycle state
    parent
  ) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // check inline-template render functions
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
  }

这个里面并没有什么特别的,看起来就是用Vue的子类实例化了一个对象,因此看起来不太像是这里,不过componentVNodeHooks.init里面用child变量接了一下最后返回的实例,并调用child.mount执行了挂载方法,需要注意,这里给child.mount执行了挂载方法,需要注意,这里给child.mount的实参vnode.elm是undefined,所以在挂载过程中,沿着下列执行路径: child.$mount(undefined) -> mountComponent(this, el, hydrating) -> new Watcher -> vm._render -> vm._update -> patch -> createElm执行

这次进入createElm里面是要创建组件节点里的DOM元素,但在执行insert(parentElm, vnode.elm, refElm)时,由于parentElm是undefined,所以无法将创建出来的真实节点插入到parentElm中,等到这次createElm执行结束后,在之后依次返回到各级函数过程中,返回到_update的时候,会给当前Vue实例对象上赋$el,即刚刚创建出来的真实DOM节点对象:

   Vue.prototype._update = function (vnode, hydrating) {
      var vm = this;
      var prevEl = vm.$el;
      var prevVnode = vm._vnode;
      var restoreActiveInstance = setActiveInstance(vm); // 每个组件渲染的时候 会将当前组件的实例暴露到全局上 Dep.target
      vm._vnode = vnode;
      // Vue.prototype.__patch__ is injected in entry points
      // based on the rendering backend used.
      if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); // 将组件渲染后的元素赋予给vm.$el
      } else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode);

再继续返回后child.$mount执行完毕,componentVNodeHooks.init也就执行完了,然后就回到了createComponent方法里面:

    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */); // init 走完以后 
        }
        // after calling the init hook, if the vnode is a child component
        // it should've created a child instance and mounted it. the child
        // component also has set the placeholder vnode's elm.
        // in that case we can just return the element and be done.
        if (isDef(vnode.componentInstance)) { // 因为实例复用了 实例上就有老的属性 
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm); // 直接将缓存的dom元素插入
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }

再通过isDef(vnode.componentInstance)这一条件,走到initComponent方法中,就会将vnode.componentInstance.$el赋值给vnode.elm

    function initComponent (vnode, insertedVnodeQueue) {
      if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
        vnode.data.pendingInsert = null;
      }
      vnode.elm = vnode.componentInstance.$el;

再执行insert(parentElm, vnode.elm, refElm)时,vnode.elm也已经有值,就将它插入成功了