前言

上回我们了解了 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 两个节点的逻辑?
3.png

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

扫描下方的二维码或搜索「tony老师的前端补习班」关注我的微信公众号,那么就可以第一时间收到我的最新文章。


tonychen
1.2k 声望272 粉丝