_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 方法接受两个参数:
vnode:VNode类型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 转换为浏览器 DOM。patch 定义在 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 方法执行后的结果,这里传入一个参数,包含 nodeOps 和 modules,nodeOps 是封装后的一系列 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 渲染也需要,因此每个平台下面都有各自的 nodeOps 和 modules,他们都在 src/platforms 文件夹下。
而不同的平台 patch 的主要逻辑是相同的,只不过部分参数不同,所以 patch 放在 core 文件夹下,对于其他平台的差异,这里用到函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前传入,这样就不用每次 pathc 的时候传入 nodeOps 和 modules 了。
回到 patch 方法,他接受 4 个参数:
oldVnode: 旧的VNode节点vnode: 新的VNode节点hydrating: 是否服务端渲染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 生成的 VNode,hydrating 为 false,removeOnly 为 false。确定入参后,我们回到 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)
);
}
因为我们传入的 oldVNode 为 DOM container,所以 isRealElement 为 true,接下来执行 emptyNodeAt 把 oldVNode 转换为 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);
}
}
最后调用 insert 把 DOM 插入到父节点中,因为是递归调用,所以一定是子节点有限调用 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 方法,这时传入的 parentElm 是 oldVnode.elm,即 id 为 #app div 的父元素,也就是 Body。
总结
至此,我们从主线上分析完了模板和数据是如何渲染成最终的浏览器 DOM,我们可以通过下图更直观的看到从初始化 Vue 到最终渲染的全过程。
这里只是分析了最简单的场景,实际项目中我们有很多组件,并组成嵌套关系,更加复杂。Vue 的另一个核心就是组件化,我们后面将从分析组件化开始。
数据驱动总流程图:
参考:Vue.js 技术揭秘