根据调试工具看Vue源码之虚拟dom(三)

栏目: 编程语言 · 发布时间: 7年前

内容简介:上回我们了解了上次我们提到的简单梳理下这段代码的逻辑:

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

Vue.prototype._update

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

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__ 函数的实现其实很简单:point_down:

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

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

patch

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

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
  }
复制代码

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

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

patch 函数的特殊逻辑

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

根据调试 <a href='https://www.codercto.com/tool.html'>工具</a> 看Vue源码之虚拟dom(三)

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

  • 若「老节点」为空,则调用 createElm(vnode, insertVnodeQueue) 来 直接创建「新节点」
  • 若「老节点」为真实存在的 dom 节点,则分成以下几步:
    • 移除 「老节点」的 SSR_ATTR 属性(若存在)
    • 判断是否正在「渲染」( hydrating
      • 是则执行 hydrate(oldvnode, vnode, insertVnodeQueue) 并判断是否执行成功
        invokeInsertHook(vnode, insertVnodeQueue, true)
        
      • 否则调用 emptyNodeAt(oldVnode) ,给「老节点」(实际上是 dom 节点)生成它的 " vnode "
根据调试工具看Vue源码之虚拟dom(三)

被「遗忘」的一行代码

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

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

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

patchVnode

「相同的节点」

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

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 两个节点的逻辑:point_down:

根据调试工具看Vue源码之虚拟dom(三)

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Facebook效应

Facebook效应

[美] 大卫·柯克帕特里克 / 沈路、梁军、崔筝 / 华文出版社 / 2010-10 / 49.80

本书作者近距离地采访了与Facebook相关的人士,其中包括Facebook的创始人、员工、投资人、意向投资人以及合作伙伴,加起来超过了130人。这是真切详实的访谈,更是超级精彩的故事。作者以其细腻的笔触,精巧的叙事结构,解密了Facebook如何从哈佛的宿舍里萌发,创始人的内讧,权力之争,如何放弃华盛顿邮报的投资,怎样争取到第一个广告客户,而第一轮融资又如何获得一亿美元的估值,让人痴迷的图片产品......一起来看看 《Facebook效应》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具