ps:建议PC端观看,移动端代码高亮错乱
1. _update
_update 被调用的时机有 2 个:
- 首次渲染
- 数据更新
由于我们这一章节只分析首次渲染部分,数据更新部分会在之后分析响应式原理的时候涉及。_update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:
// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// ...
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 响应式更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
}
用 preVnode 来判断是否是首次渲染。
__patch__ 函数在不同平台会有不同的定义,web 端的定义在 src/platforms/web/runtime/index.js 文件中:
// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
如果是浏览器环境,则是 patch 方法,否则是一个空函数。
而 patch 函数,它被定义在 src/platforms/web/runtime/patch.js 文件中:
// src/platforms/web/runtime/patch.js
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封装了平台的DOM操作方法。modules表示平台的模块,它们会在整个patch过程的不同阶段执行相应的钩子函数。
这样通过函数柯里化, createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了,可以抹去不同平台的差异,真正的 patch 函数不需要关心这些差异。

下面就来看看 createPatchFunction 是怎么创建并返回真正的 patch 函数吧,
定义在:src/core/vdom/patch.js
// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 遍历,将 hooks 作为 cbs 属性,然后将对应的 modules 的子项 push 到 cbs.hooks 中。
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
return function patch(oldVnode, vnode, hydarting, removeOnly) {
...
}
}
2. patch函数
因为 patch 的逻辑非常多且复杂,所以我们结合实际的例子来分析首次渲染的 patch 大致干了啥:
<body>
<div id="app"></div>
</body>
var app = new Vue({
el: '#app',
render: function(createElement) {
return createElement('div', {
attrs: { id: 'app' }
}, 'Hello Vue!')
}
})
当初次渲染流程执行到 vm._update 的时:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
所以传入 patch 的参数分别是:
oldVnode:例子中id为app的DOM对象,也就是在 HTML 模板中写的<div id="app">,vm.$el的赋值是在之前mountComponent函数做的。vnode:调用render函数的返回值hydrating:false,表示非服务端情况removeOnly:是给transition-group用的,之后会介绍。
确定了这些入参后,我们进入 patch 函数的执行过程,看几个关键步骤。
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
return function patch(oldVnode, vnode, hydarting, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// ...
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ...
} else {
if (isRealElement) {
// ...
// 将真实的 DOM 转换成 vnode,也就是 <div id="app"></div>
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm // 保存真实的DOM
const parentElm = nodeOps.parentNode(oldElm) // body
// oldEm._leaveCb 在这是 undefined
// insertedVnodeQueue 在这是空数组
// nextSibling表示DOM的右边的节点,在这是换行text节点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
// ...
}
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// ...
return vnode.elm
}
}
oldVnode是真实的DOM,通过emptyNodeAt将真实的DOM转换成vnodeparentElm在这个例子中是body节点(<body><div id="app"></div></body>)- 调用
createElm函数,它的作用是通过vnode创建真实的DOM并插入到它的父节点中 vnode.parent,这是父占位节点。和组件相关的,这里不会执行,也就不展开细讲。- 判断之前定义的
parentElm是否存在,有则删除掉vm.$el对应的节点。在执行这一步前,浏览器的 DOM 结构是这样的:
<body>
<div id="app"></div>
<div id="app">Hello Vue!</div>
</body>
之后删除 <div id="app"></div> 完成新旧节点替换工作。
- 最后将
vnode.elm(也就是真实DOM)返回。
2.1 emptyNodeAt
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
// ...
}
2.2 createElm
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
// ...
// 尝试创建组件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// ...
// 调用平台 DOM 的操作去创建一个占位符元素。
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// ...
if (__WEEX__) {
// ...
} else {
// 创建子元素
createChildren(vnode, children, insertedVnodeQueue)
// ...
insert(parentElm, vnode.elm, refElm)
}
// ...
} 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)
}
}
// ...
}
createComponent方法目的是尝试创建子组件,这个逻辑在之后组件的章节会详细介绍,在当前例子中返回falsenodeOps.createElement实际上就是通过document.createElement创建一个元素,并赋值给vnode.elmcreateChildren创建子元素,遍历子虚拟节点,递归调用createElm,遍历过程中会把vnode.elm作为父容器的DOM节点占位符传入。- 最后调用
insert方法把DOM插入到父节点中,因为是递归调用,子元素会优先调用insert,所以整个vnode树节点的插入顺序是先子后父。
2.2.1 createChildren
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
// ...
for (let i = 0; i < children.length; ++i) {
// 递归调用createElm
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
// ...
}
2.2.2 insert
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
// ...
}
总结
在 patch 方法,首次渲染我们调用了 createElm 方法,这里传入的 parentElm 是 oldVnode.elm 的父元素,在我们的例子是 id 为 #app 的 div 的父元素,也就是 Body;实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。
