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

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

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

上回我们了解了 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(三)

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


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

查看所有标签

猜你喜欢:

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

Ruby on Rails电子商务实战

Ruby on Rails电子商务实战

Christian Hellsten、Jarkko Laine / 曹维远 / 人民邮电出版社 / 2008-4 / 49.00元

《Ruby on Rails电子商务实战》全面讲解了使用Ruby on Rails创建产品级应用程序的过程。书中通过演示构建网上书店的全过程,先后介绍如何使用如TDD的敏捷实践,启动一个项目并建立良好稳定的基础,如何深入Ruby on Rails,实现诸如将应用程序翻译成各种语言对产品进行调试等的普遍需求。其中用到的主要技术包括Ajax、聚合、设置标签和国际化等,还介绍了如何使用ActiveRec......一起来看看 《Ruby on Rails电子商务实战》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具