• vnode之update 还是没太懂


     

    Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候;这分析首次渲染部分,数据更新部分会在之后分析响应式原理的时候涉及。

    _update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:

    _update 的核心就是调用 vm.__patch__ 方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js 中:

    function lifecycleMixin (Vue) {
        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

    可以看到,甚至在 web 平台上,是否是服务端渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器 DOM 环境,

    所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch 方法,它的定义在 src/platforms/web/runtime/patch.js中:

            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); //核心  这里 Vue.prototype.__patch__ = inBrowser ? patch : noop 
          } 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.
        };
    
        Vue.prototype.$forceUpdate = function () {
          var vm = this;
          if (vm._watcher) {
            vm._watcher.update();
          }
        };
    
        Vue.prototype.$destroy = function () {
          var vm = this;
          if (vm._isBeingDestroyed) {
            return
          }
          callHook(vm, 'beforeDestroy');
          vm._isBeingDestroyed = true;
          // remove self from parent
          var parent = vm.$parent;
          if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
            remove(parent.$children, vm);
          }
          // teardown watchers
          if (vm._watcher) {
            vm._watcher.teardown();
          }
          var i = vm._watchers.length;
          while (i--) {
            vm._watchers[i].teardown();
          }
          // remove reference from data ob
          // frozen object may not have observer.
          if (vm._data.__ob__) {
            vm._data.__ob__.vmCount--;
          }
          // call the last hook...
          vm._isDestroyed = true;
          // invoke destroy hooks on current rendered tree
          vm.__patch__(vm._vnode, null);
          // fire destroyed hook
          callHook(vm, 'destroyed');
          // turn off all instance listeners.
          vm.$off();
          // remove __vue__ reference
          if (vm.$el) {
            vm.$el.__vue__ = null;
          }
          // release circular reference (#6759)
          if (vm.$vnode) {
            vm.$vnode.parent = null;
          }
        };
      }
    
    

    该方法的定义是调用 createPatchFunction 方法的返回值,这里传入了一个对象,包含 nodeOps 参数和 modules 参数。

    其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现,我们这里先不详细介绍,来看一下 createPatchFunction 的实现,它定义在 src/core/vdom/patch.js中:

    var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
    const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
    
    

    createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__

     

    在介绍 patch 的方法实现之前,我们可以思考一下为何 Vue.js 源码绕了这么一大圈,把相关代码分散到各个目录。因为前面介绍过,patch 是平台相关的,在 Web 和 Weex 环境,

    它们把虚拟 DOM 映射到 “平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。因此每个平台都有各自的 nodeOps 和 modules,它们的代码需要托管在 src/platforms 这个大目录下。

     

    而不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共的部分托管在 core 这个大目录下。差异化部分只需要通过参数来区别,这里用到了一个函数柯里化的技巧,

    通过 createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了,这种编程技巧也非常值得学习。

     

    在这里,nodeOps 表示对 “平台 DOM” 的一些操作方法,modules 表示平台的一些模块,它们会在整个 patch 过程的不同阶段执行相应的钩子函数。

     

    function createPatchFunction (backend) {
        var i, j;
        var cbs = {};
    
        var modules = backend.modules;
        var nodeOps = backend.nodeOps;
    
        for (i = 0; i < hooks.length; ++i) {
          cbs[hooks[i]] = [];
          for (j = 0; j < modules.length; ++j) {
            if (isDef(modules[j][hooks[i]])) {
              cbs[hooks[i]].push(modules[j][hooks[i]]);
            }
          }
        }

    回到 patch 方法本身,它接收 4个参数,oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;vnode 表示执行 _render 后返回的 VNode 的节点;

    hydrating 表示是否是服务端渲染;removeOnly 是给 transition-group 用的

    由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,

    然后再调用 createElm 方法,这个方法在这里非常重要:

    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 {
                    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([oldVnode], 0, 0);
              } else if (isDef(oldVnode.tag)) {
                invokeDestroyHook(oldVnode);
              }
            }
          }
    
          invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
          return vnode.elm
        }
      }

    createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 我们来看一下它的一些关键逻辑,createComponent 方法目的是尝试创建子组件,

    在当前这个 case 下它的返回值为 false;接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。


    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$$1(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); setScope(vnode); /* istanbul ignore if */ { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); } if (data && data.pre) { creatingElmInVPre--; } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }

    createChildren 的逻辑很简单,实际上是遍历子虚拟节点,递归调用 createElm,这是一种常用的深度优先的遍历算法,这里要注意的一点是在遍历过程中会把 vnode.elm 作为父容器的 DOM 节点占位符传入。

    接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中。

    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)));
          }
        }
    

    最后调用 insert 方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父。它的定义在 src/core/vdom/patch.js 上。

    function invokeCreateHooks (vnode, insertedVnodeQueue) {
          for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
            cbs.create[i$1](emptyNode, vnode);
          }
          i = vnode.data.hook; // Reuse variable
          if (isDef(i)) {
            if (isDef(i.create)) { i.create(emptyNode, vnode); }
            if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
          }
        }

    insert 逻辑很简单,调用一些 nodeOps 把子节点插入到父节点中,这些辅助方法定义在 src/platforms/web/runtime/node-ops.js 中:

      function insert (parent, elm, ref$$1) {
          if (isDef(parent)) {
            if (isDef(ref$$1)) {
              if (nodeOps.parentNode(ref$$1) === parent) {
                nodeOps.insertBefore(parent, elm, ref$$1);
              }
            } else {
              nodeOps.appendChild(parent, elm);
            }
          }
        }

    其实就是调用原生 DOM 的 API 进行 DOM 操作

     createElm 过程中,如果 vnode 节点如果不包含 tag,则它有可能是一个注释或者纯文本节点,可以直接插入到父元素中。在我们这个例子中,

    最内层就是一个文本 vnode,它的 text 值取的就是之前的 this.message 的值 Hello Vue!

    再回到 patch 方法,首次渲染我们调用了 createElm 方法,这里传入的 parentElm 是 oldVnode.elm 的父元素,

    在我们的例子是 id 为 #app div 的父元素,也就是 Body;实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。

    最后,我们根据之前递归 createElm 生成的 vnode 插入顺序队列,执行相关的 insert 钩子函数

     function insertBefore (parentNode, newNode, referenceNode) {
        parentNode.insertBefore(newNode, referenceNode);
      }
    function appendChild (node, child) {
        node.appendChild(child);
      }
  • 相关阅读:
    Individual Method Selection Survey rubric
    Xcode 出现Thread 1: signal SIGABRT
    C/C++生成随机数
    《深入浅出深度学习:原理剖析与python实践》第八章前馈神经网络(笔记)
    操作系统--精髓与设计原理(第八版)第三章复习题答案
    操作系统--精髓与设计原理(第八版)第二章复习题答案
    Python知识点整理
    C++ <queue>用法
    C语言结构体用法
    Mac安装Qt出现错误Could not resolve SDK Path for 'macosx'
  • 原文地址:https://www.cnblogs.com/TTblog5/p/12863365.html
Copyright © 2020-2023  润新知