前言

上回我们了解了 vnode 从创建到生成的流程,这回我们来探索 Vue 是如何将 vnode 转化成真实的 dom 节点/元素

Vue.prototype._update

上次我们提到的 _render 函数其实作为 _update 函数的参数传入,换句话说,_render 函数结束后 _update 将会执行????

Vue.prototype._update = function (vnode, hydrating) {    var vm = this;    var prevEl = vm.$el;    var prevVnode = vm._vnode;    var restoreActiveInstance = setActiveInstance(vm);    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 */);    } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode);    }    restoreActiveInstance();    // update __vue__ reference    if (prevEl) {      prevEl.__vue__ = null;    }    if (vm.$el) {      vm.$el.__vue__ = vm;    }    // if parent is an HOC, update its $el as well    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {      vm.$parent.$el = vm.$el;    }    // updated hook is called by the scheduler to ensure that children are    // updated in a parent's updated hook.  };

简单梳理下这段代码的逻辑:

  • 调用 setActiveInstance(vm) 设置当前的 vm 为活跃的实例
  • 判断 preVnode 是否存在,是则调用 vm.$el = vm.__patch__(prevVnode, vnode);,否则调用 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);(其实也就是第一次渲染跟二次更新的区别)
  • 调用 restoreActiveInstance() 重置活跃的实例
  • HOC 做了特殊判断(因为没用过 HOC,所以这里直接略过)

从上面整理下来的逻辑中,我们能得到讯息仅仅只有 setActiveInstance 函数返回一个闭包函数(当然这并不是很重要),如果需要更深入的了解,还需要了解 __patch__ 函数是怎么实现的

其他相关代码:

updateComponent = function () {  vm._update(vm._render(), hydrating);};...new Watcher(vm, updateComponent, noop, {    before: function before () {      if (vm._isMounted && !vm._isDestroyed) {        callHook(vm, 'beforeUpdate');      }    }  }, true /* isRenderWatcher */);

__patch__

说出来你可能不信,__patch__ 函数的实现其实很简单????

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });...Vue.prototype.__patch__ = inBrowser ? patch : noop;

很明显,createPatchFunction 也是返回了一个闭包函数

patch

虽然 __patch__ 外表看起来很简单,但是其实内部实现的逻辑还是挺复杂的,代码量也非常多????

return function patch (oldVnode, vnode, hydrating, removeOnly) {    if (isUndef(vnode)) {      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }      return    }    var isInitialPatch = false;    var insertedVnodeQueue = [];    if (isUndef(oldVnode)) {      // empty mount (likely as component), create new root element      isInitialPatch = true;      createElm(vnode, insertedVnodeQueue);    } else {      var isRealElement = isDef(oldVnode.nodeType);      if (!isRealElement && sameVnode(oldVnode, vnode)) {        // patch existing root node        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);      } else {        if (isRealElement) {          // mounting to a real element          // check if this is server-rendered content and if we can perform          // a successful hydration.          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {            oldVnode.removeAttribute(SSR_ATTR);            hydrating = true;          }          if (isTrue(hydrating)) {            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {              invokeInsertHook(vnode, insertedVnodeQueue, true);              return oldVnode            } else if (process.env.NODE_ENV !== 'production') {              warn(                'The client-side rendered virtual DOM tree is not matching ' +                'server-rendered content. This is likely caused by incorrect ' +                'HTML markup, for example nesting block-level elements inside ' +                '<p>, or missing <tbody>. Bailing hydration and performing ' +                'full client-side render.'              );            }          }          // either not server-rendered, or hydration failed.          // create an empty node and replace it          oldVnode = emptyNodeAt(oldVnode);        }        // replacing existing element        var oldElm = oldVnode.elm;        var parentElm = nodeOps.parentNode(oldElm);        // create new node        createElm(          vnode,          insertedVnodeQueue,          // extremely rare edge case: do not insert if old element is in a          // leaving transition. Only happens when combining transition +          // keep-alive + HOCs. (#4590)          oldElm._leaveCb ? null : parentElm,          nodeOps.nextSibling(oldElm)        );        // update parent placeholder node element, recursively        if (isDef(vnode.parent)) {          var ancestor = vnode.parent;          var patchable = isPatchable(vnode);          while (ancestor) {            for (var i = 0; i < cbs.destroy.length; ++i) {              cbs.destroy[i](ancestor);            }            ancestor.elm = vnode.elm;            if (patchable) {              for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {                cbs.create[i$1](emptyNode, ancestor);              }              // #6513              // invoke insert hooks that may have been merged by create hooks.              // e.g. for directives that uses the "inserted" hook.              var insert = ancestor.data.hook.insert;              if (insert.merged) {                // start at index 1 to avoid re-invoking component mounted hook                for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {                  insert.fns[i$2]();                }              }            } else {              registerRef(ancestor);            }            ancestor = ancestor.parent;          }        }        // destroy old node        if (isDef(parentElm)) {          removeVnodes(parentElm, [oldVnode], 0, 0);        } else if (isDef(oldVnode.tag)) {          invokeDestroyHook(oldVnode);        }      }    }    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);    return vnode.elm  }

这么多的代码,一下子肯定是消化不完的,所以我们可以尝试性的带着以下这几个问题来看????

  • 第一次的 patch 操作与后续的 patch 操作有何区别?
  • dom 节点之间产生变更,或者说是「新节点」替换「老节点」时,规则是怎么样的?

patch 函数的特殊逻辑

针对初次渲染,patch 函数是做了特殊逻辑的。显然我们只要把初次执行的 patch 的逻辑走一遍就清楚了????

结合上面的源码,归纳下这里的思路:

  • 若「老节点」为空,则调用 createElm(vnode, insertVnodeQueue)来 直接创建「新节点」
  • 若「老节点」为真实存在的 dom 节点,则分成以下几步:

    • 移除 「老节点」的 SSR_ATTR 属性(若存在)
    • 判断是否正在「渲染」(hydrating

      • 是则执行hydrate(oldvnode, vnode, insertVnodeQueue)并判断是否执行成功

        • 成功后触发 invokeInsertHook(vnode, insertVnodeQueue, true)
        • 失败后发出「警告」(测试环境)
      • 否则调用 emptyNodeAt(oldVnode),给「老节点」(实际上是 dom 节点)生成它的 "vnode"

被「遗忘」的一行代码

看完源码的同学不难不发现,上面梳理的逻辑里少了这段代码:

if (!isRealElement && sameVnode(oldVnode, vnode)) {    // patch existing root node    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);}

也就是对「非 dom 元素的相同节点」做一次 patchVnode 的操作。关于这段代码可以分成几点来分析:

  • 什么是「相同节点」?
  • patchVnode 做了什么?

「相同的节点」

根据语义我们应该看这部分代码????

function sameVnode (a, b) {  return (    a.key === b.key && (      (        a.tag === b.tag &&        a.isComment === b.isComment &&        isDef(a.data) === isDef(b.data) &&        sameInputType(a, b)      ) || (        isTrue(a.isAsyncPlaceholder) &&        a.asyncFactory === b.asyncFactory &&        isUndef(b.asyncFactory.error)      )    )  )}

sameVnode 的逻辑就是:按照 vnode 的属性来判断两个 「vnode」节点是否是同一个节点

patchVnode

由于执行 patchVnode 的前提就是新老节点是「相同」的节点,我们有理由相信,它是用来处理同个节点的变化。

function patchVnode (    oldVnode,    vnode,    insertedVnodeQueue,    ownerArray,    index,    removeOnly  ) {    if (oldVnode === vnode) {      return    }    if (isDef(vnode.elm) && isDef(ownerArray)) {      // clone reused vnode      vnode = ownerArray[index] = cloneVNode(vnode);    }    var elm = vnode.elm = oldVnode.elm;    if (isTrue(oldVnode.isAsyncPlaceholder)) {      if (isDef(vnode.asyncFactory.resolved)) {        hydrate(oldVnode.elm, vnode, insertedVnodeQueue);      } else {        vnode.isAsyncPlaceholder = true;      }      return    }    // reuse element for static trees.    // note we only do this if the vnode is cloned -    // if the new node is not cloned it means the render functions have been    // reset by the hot-reload-api and we need to do a proper re-render.    if (isTrue(vnode.isStatic) &&      isTrue(oldVnode.isStatic) &&      vnode.key === oldVnode.key &&      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))    ) {      vnode.componentInstance = oldVnode.componentInstance;      return    }    var i;    var data = vnode.data;    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {      i(oldVnode, vnode);    }    var oldCh = oldVnode.children;    var ch = vnode.children;    if (isDef(data) && isPatchable(vnode)) {      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }    }    if (isUndef(vnode.text)) {      if (isDef(oldCh) && isDef(ch)) {        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }      } else if (isDef(ch)) {        if (process.env.NODE_ENV !== 'production') {          checkDuplicateKeys(ch);        }        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);      } else if (isDef(oldCh)) {        removeVnodes(elm, oldCh, 0, oldCh.length - 1);      } else if (isDef(oldVnode.text)) {        nodeOps.setTextContent(elm, '');      }    } else if (oldVnode.text !== vnode.text) {      nodeOps.setTextContent(elm, vnode.text);    }    if (isDef(data)) {      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }    }  }

我们看看这段代码都做了哪些事情:

  1. 复用 vnode(如果存在 elem 属性)
  2. 处理异步组件
  3. 处理静态节点
  4. 执行 prepatch(如果存在 data 属性)
  5. 执行 update(如果存在 data 属性)
  6. 比较 oldVnodevnode 两个节点
  7. 执行 postpatch(如果存在 data 属性)

当然,这里最直观的就是比较 oldVnodevnode 两个节点的逻辑????

其他的逻辑可以留到下一篇文章再分析~