【Vue源码】7._update

186 阅读3分钟

_update

3.Vue 实例挂载的实现 中我们看到最后 mountComponent 方法调用的是 _render_update 这两个方法。

_update 是实例的私有方法,作用是把 VNode 渲染成真实的浏览器 DOM,定义在 src/core/instance/lifecycle.js 中。他有两个调用的地方,一是首次渲染,二是在数据更新的时候调用。因为我们目前只分析首次渲染部分,数据更新后面在看。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  const prevActiveInstance = activeInstance;
  activeInstance = 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);
  }
  activeInstance = prevActiveInstance;
  // 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.
};

_update 方法接受两个参数:

  1. vnode: VNode 类型
  2. hydrating: 是否为服务器渲染

根据调用之前 vm._vnode 来判断是否为首次渲染。核心是调用 vm._patch 方法,这个方法定义在 src/platforms/web/runtime/index.js

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;

__patch__ 会根据不同平台返回不同的方法,如果是浏览器会返回 patch,否在返回 noop 空函数,因为在服务器渲染中,没有真实的浏览器环境,所以不需要把 VNode 转换为浏览器 DOMpatch 定义在 src/platforms/web/runtime/patch.js 中。

patch

/* @flow */

import * as nodeOps from "web/runtime/node-ops";
import { createPatchFunction } from "core/vdom/patch";
import baseModules from "core/vdom/modules/index";
import platformModules from "web/runtime/modules/index";

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules);

export const patch: Function = createPatchFunction({ nodeOps, modules });

patch 方法返回由 createPatchFunction 方法执行后的结果,这里传入一个参数,包含 nodeOpsmodulesnodeOps 是封装后的一系列 DOM 操作的方法,而 modules 则定义了一些函数钩子的实现,暂且不表。来看一下 createPatchFunction 方法的实现,定义在 src/core/vdom/patch.js 中。

createPatchFunction

const hooks = ["create", "activate", "update", "remove", "destroy"];

export function createPatchFunction(backend) {
  let i, j;
  const cbs = {};

  const { modules, nodeOps } = backend;

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

  // ...

  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
      return;
    }

    let isInitialPatch = false;
    const insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      const isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, 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
        const oldElm = oldVnode.elm;
        const 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)) {
          let ancestor = vnode.parent;
          const patchable = isPatchable(vnode);
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor);
            }
            ancestor.elm = vnode.elm;
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor);
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert;
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]();
                }
              }
            } 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;
  };
}

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

在这里我们先思考一下,为什么 Vue.js 的源码绕来绕去,把相关的代码放在不同的文件夹。因为前面介绍过,patch 是平台相关的,不止 web 需要,服务端Weex 渲染也需要,因此每个平台下面都有各自的 nodeOpsmodules,他们都在 src/platforms 文件夹下。

而不同的平台 patch 的主要逻辑是相同的,只不过部分参数不同,所以 patch 放在 core 文件夹下,对于其他平台的差异,这里用到函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前传入,这样就不用每次 pathc 的时候传入 nodeOpsmodules 了。

回到 patch 方法,他接受 4 个参数:

  1. oldVnode: 旧的 VNode 节点
  2. vnode: 新的 VNode 节点
  3. hydrating: 是否服务端渲染
  4. removeOnly: 给 transition-group

回到 vm._update 方法执行调用 vm.__patch__ 的地方

// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);

我们可以在执行 patch 方法时 oldVNode 即为 vm.$el(<div id="app">),vnode 为调用 vm._render 生成的 VNodehydrating falseremoveOnlyfalse。确定入参后,我们回到 patch 方法看关键的几个地方。

const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, 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
  const oldElm = oldVnode.elm;
  const 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)
  );
}

因为我们传入的 oldVNodeDOM container,所以 isRealElement true,接下来执行 emptyNodeAtoldVNode 转换为 VNode 对象,然后再调用 createElem 方法。createElem 非常重要,看一下方法里面的具体细节。

createElem

function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  debugger;
  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;
  }

  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  if (isDef(tag)) {
    // ...
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
    setScope(vnode);

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      insert(parentElm, vnode.elm, refElm);
    }

    if (process.env.NODE_ENV !== "production" && 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);
  }
}

createElem 的作用是把虚拟节点创建为真实 浏览器 DOM 并插入到节点中。 如果 tag 存在,则根据 nodeOps 创建一个占位符。接下来调用 createChildren 方法创建子元素:

function createChildren(vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== "production") {
      checkDuplicateKeys(children);
    }
    for (let 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)));
  }
}

createChildren 方法的作用是遍历子节点,递归的调用 createElm,这是深度优先的遍历算法。这里要注意一点就是遍历的过程中会把 vnode.elm 作为父容器的 DOM 节点传入。

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

function invokeCreateHooks(vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](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);
  }
}

最后调用 insertDOM 插入到父节点中,因为是递归调用,所以一定是子节点有限调用 insert,所以整个 vnode 树节点的插入顺序是先子后父。顺带提一嘴,Vue 的父子组件的加载也是这个顺序,就是当子节点的生命周期执行完 mounted 后父节点才继续执行 mounted

来看一下 insert 方法。

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

主要作用就是调用 nodeOps 的方法,把子节点插入到父节点中。nodeOps 内的一些方法定义在 src/platforms/web/runtime/node-ops.js 中。

export function insertBefore(
  parentNode: Node,
  newNode: Node,
  referenceNode: Node
) {
  parentNode.insertBefore(newNode, referenceNode);
}

export function appendChild(node: Node, child: Node) {
  node.appendChild(child);
}

createElm 过程中,如果 vnode 上没有 tag 则创建注释或者文字节点。

再回到 patch 方法,我们的首次渲染调用了 createElm 方法,这时传入的 parentElmoldVnode.elm,即 id#app div 的父元素,也就是 Body

总结

07.数据驱动-_update.png

至此,我们从主线上分析完了模板和数据是如何渲染成最终的浏览器 DOM,我们可以通过下图更直观的看到从初始化 Vue 到最终渲染的全过程。

这里只是分析了最简单的场景,实际项目中我们有很多组件,并组成嵌套关系,更加复杂。Vue 的另一个核心就是组件化,我们后面将从分析组件化开始。

数据驱动总流程图:

07.流程图.png

源码分析 GitHub 地址

参考:Vue.js 技术揭秘