系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
在上一章节中,通过调用_render方法,最终生成了一个VNode节点,那么接下来,就会调用_update方法,Vue会根据这个VNode渲染成真实的DOM。
_update
_update的代码如下所示:
/* 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 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.
}
可以看到,在_update方法中,Vue会根据是否是初次渲染,使用不同的参数调用__patch__方法,那么接下来,我们就来详细看看其内部是如何实现的。
patch
在看patch的实现之前,需要了解patch方法是如何构建出来的,因为对于VNode来说,它只是一个节点的描述符,而不同的平台需要使用各自原生的方法对VNode进行渲染,而对于Web平台来说,patch方法是在引入Vue时添加到原型上的,代码如下所示:
/* platforms/web/runtime/patch.js */
export const patch: Function = createPatchFunction({ nodeOps, modules })
/* platforms/web/runtime/index.js */
Vue.prototype.__patch__ = inBrowser ? patch : noop
可以看到,patch又是通过调用createPatchFunction方法构建的,而这里的nodeOps和modules,都是与当前平台相关的代码,所以不同的平台只需要使用与之对应的nodeOps和modules,就可以使用同一个createPatchFunction方法构建出适合当前平台的patch方法。createPatchFunction的源码在core/vdom/patch.js中,最终返回的patch方法如下所示:
/* core/vdom/patch.js */
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, null, null, removeOnly)
} else {
if (isRealElement) {
// ...
// 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([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
可以看到,patch方法还是比较复杂的,因为里面包含了创建、更新、删除等逻辑,我们可以将其分为以下四种情况:
-
卸载组件:如果
vnode不存在,说明当前组件没有需要渲染的内容,同时,如果oldVnode存在,就调用invokeDestroyHook方法,执行oldVnode的卸载逻辑:if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }function invokeDestroyHook(vnode) { let i, j const data = vnode.data if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } } -
组件的首次渲染:如果
oldVnode不存在,说明是组件的首次渲染,首先将标志位isInitialPatch设置为true,然后调用createElm方法,根据vnode生成真实的DOM,最后还会调用invokeInsertHook方法,将其内部包含的子组件添加到父占位符节点的data.pendingInsert中:let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)function invokeInsertHook(vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { // ... } }当程序回到组件的父占位符节点时,又会调用
initComponent方法,将pendingInsert中的节点提取到insertedVnodeQueue中:function initComponent(vnode, insertedVnodeQueue) { if (isDef(vnode.data.pendingInsert)) { insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) vnode.data.pendingInsert = null } // ... }经过这样一层层的提取,就可以在根节点的
insertedVnodeQueue中,统一执行insert钩子函数。 -
组件的对比更新:如果新旧节点都存在,并且通过
sameVnode检测到它们是相同的节点,那么就不需要重新创建一个全新的DOM了,而是复用此DOM,然后通过对比vnode和oldVnode中的data和children,做更新操作:const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) }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) ) ) ) } -
创建新节点并移除旧节点:如果以上的情况都不满足,说明需要根据
vnode重新创建新的节点,然后根据oldVnode删除旧的节点,从而达到页面更新的逻辑。
从上面的代码中可以看出,patch中最关键的两个方法是createElm和patchVnode,这两个方法会在之后的两小节中详细介绍。在这之前,我们先来看看在执行patch的过程中,都运行了哪些hook。
hooks
在patch的过程中,其实包含了两种类型的hook,一种是与VNode相关的hook,一种是modules相关的hook。
VNode相关的hook很好理解,在之前创建组件VNode时就已经见过了,这种类型的hook都是与VNode的操作相关的。
modules相关的hook主要是用来处理vnode.data中的数据,它会在创建patch方法时,进行预处理操作:
/* platforms/web/runtime/patch.js */
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]])
}
}
}
}
由于这里modules代表的是平台相关的模块,所以对于Web平台来说中,Vue支持对以下8种模块的处理,如下所示:

可以看到,这里的模块都可以从VNodeData中找到映射,而且这些modules钩子函数也是伴随VNode hook进行处理的。
总结
在patch方法中,Vue会根据新旧vnode的状态,执行不同的操作,同时在创建、更新、删除节点的时候,执行相应的VNode钩子函数,同时也会根据modules钩子函数处理vnode.data中的数据,将其作用在DOM上。