建议PC端观看,移动端代码高亮错乱

1. 介绍
当数据发生变化的时候,会触发 渲染watcher 的回调函数,进而执行组件的更新过程。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
组件的更新还是调用了 vm._update 方法,而 vm._update 会执行 vm.$el = vm.__patch__(prevVnode, vnode),它最终仍然会调用 patch 函数,在 src/core/vdom/patch.js 中定义:
function patch (
oldVnode,
vnode,
hydrating, // undefined
removeOnly // undefined
) {
// ...
const insertedVnodeQueue = [] // 存放占位符vnode,调用子组件的mounted
if (isUndef(oldVnode)) {
// ...
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 一、新旧vnode相同的情况
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
// 二、新旧vnode不同的情况
} else {
// 2.1. 创建新节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
)
// 2.2. 递归更新父的占位符节点
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode) // 是否可挂载
while (ancestor) {
// 遍历 cbs.destroy,依次destroy执行钩子...
ancestor.elm = vnode.elm // 在创建新节点的步骤中会给 elm 赋值
if (patchable) {
// 遍历cbs.create,依次执行create钩子...
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 2.3 删除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0) // 里面会调用module和vnode的 destroy钩子
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode) // 调用module和vnode的 destroy钩子
}
}
}
// ...
return vnode.elm
}
这里执行 patch 的逻辑和首次渲染是不一样的,因为 oldVnode 不为空,并且它和 vnode 都是 VNode 类型,接下来会通过 sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode 来决定走不同的更新逻辑:
// src/core/vdom/patch.js
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)
)
)
)
}
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
- 如果两个
vnode的key不相等,则肯定是不同的 - 否则继续判断对于同步组件,则判断
isComment、data、input类型等是否相同 - 对于异步组件,则判断
asyncFactory是否相同。
所以根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑,我们先来说一下不同的情况。
2. 新旧vnode不同
其实在日常开发中基本上不会走到这个逻辑,只有当我们这么编写组件时:
<template>
<div v-if="flag">
</div>
<ul v-else>
<li>1</li>
<li>2</li>
</ul>
</template>
<script>
export default {
data() {
return {
flag: true
}
}
};
</script>
由于我们在最外层节点用了 v-if,所以会产生新旧节点不同的情况,但是通常我们都是在最外层用一个标签包裹的,也就是说只有一个根元素。
回到 patch 函数,来看看新旧节点不同的情况,这部分逻辑分为三部分:
- 创建新节点
- 递归更新父的占位符节点
- 删除旧节点
2.1 创建新节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
)
- 以旧
vnode的DOM为基础获得父节点。 - 调用
createElm:通过旧vnode创建真实的DOM并插入到它的父节点中。
2.2 递归更新父的占位符节点
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
// 遍历 cbs.destroy,依次destroy执行钩子...
ancestor.elm = vnode.elm
// 是否可挂载
if (patchable) {
// 遍历cbs.create,依次执行create钩子...
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
我们只关注主要逻辑即可:获取当前 渲染vnode 的 父占位符vnode (如果存在的话),先执行各个 module 的 destroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 module 的 create 钩子函数。对于这些钩子函数的作用,在之后的章节会详细介绍。
通常情况下 while 循环只执行一次:
// parent.vue
<template>
<div>
parent
<Child></Child>
</div>
</template>
// child.vue
<template>
<div>child</div>
</template>
只有以下这种情况 while 循环会多次执行
// parent.vue
<template>
<Child></Child>
</template>
// child.vue
<template>
<div>child</div>
</template>
也就是说 父占位符vnode 同时又是一个 渲染vnode 的情况。
通过 isPatchable 函数用于判断是否可挂载,源码如下:
function isPatchable (vnode) {
// 存在 componentInstance 表示当前的渲染vnode,同时也是另一个组件的占位符vnode
// 这种情况则循环,直到找到最深层的组件
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode
}
return isDef(vnode.tag)
}
2.3 删除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
如果存在 parentElm 时:
- 把
oldVnode从当前DOM树中删除,这其中执行destroy钩子。
否则直接执行 destroy 钩子
3. 新旧vnode相同
在 patch 函数中,当新旧 vnode 相同时:
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
执行了 patchVnode 方法,参数我们只关注 oldVnode & vnode 即可。
// src/core/vdom/patch.js
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// ...
// 执行 prepatch 钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 执行 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// patch 过程
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// 执行 postpatch 钩子函数
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode 函数的主要做了这几件事:
- 执行
prepatch钩子函数,这部分在下一章再结合props展开分析 - 会执行所有
module的update钩子函数以及用户自定义的update钩子函数 - 核心
patch过程 - 执行
postpatch钩子函数
我们本章重点来关注核心的 patch 过程
// patch 过程
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
-
如果
vnode不是文本节点,则判断它们的子节点,并分了几种情况处理:oldCh与ch都存在且不相同时,使用updateChildren函数来更新子节点,这个稍后重点讲。- 如果只有
ch存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过addVnodes将ch批量插入到新节点elm下。 - 如果只有
oldCh存在,表示更新的是空节点,则需要将旧的节点通过removeVnodes全部清除。 - 当只有旧节点是文本节点的时候,则清除其节点文本内容。
-
否则
vnode是个文本节点且新旧文本不相同时,直接替换文本内容。
4. updateChildren
先贴出完整代码,再结合图片一步一步来看。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartIdx = 0
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
// 这里 canMove 为 true
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh) // 检查重复的key
}
// 循环条件
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 旧头 === 新头
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 旧尾 === 新尾
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 旧头 === 新尾
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 旧尾 === 新头
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 其他情况
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 新vnode在oldCh的index
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 新节点比旧节点多,批量增加节点
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
// 老节点比新节点多,批量删除节点
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
3.1 变量介绍
开始之前定义了一系列的变量,分别如下:
oldStartIdx:oldCh的开始指针,对应的vnode是oldStartVnodeoldEndIdx:oldCh的结束指针,对应的vnode是oldEndVnodenewStartIdx:ch的开始指针,对应的vnode是newStartVnodenewEndIdx:ch的结束指针,对应的vnode是newEndVnodeoldKeyToIdx是一个map,其中key就是常在for循环中写的key的值,value就是当前vnode,也就是可以通过唯一的key,在map中找到对应的vnode

3.2 循环条件
接下来是一个 while 循环,在这过程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 会逐渐向中间靠拢。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

首先当 oldStartVnode 或者 oldEndVnode 不存在的时候,oldStartIdx 与 oldEndIdx 继续向中间靠拢,并更新对应的 oldStartVnode 与 oldEndVnode 的指向
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
接下来是将 oldStartVode、newStartVode、oldEndVode 以及 newEndVode 两两比对的过程,一共会出现 2*2=4 种情况。
3.2 旧头 === 新头
首先是 oldStartVnode 与 newStartVnode 符合 sameVnode 时,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位。
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}

3.3 旧尾 === 新尾
其次是 oldEndVnode 与 newEndVnode 符合 sameVnode,同样进行 patchVnode 操作并将 oldEndVnode 与 newEndVnode 向前移动一位。
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}

3.4 旧头 === 新尾
先是 oldStartVnode 与 newEndVnode 符合 sameVnode 的时候,将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}

3.5 旧尾 === 新头
同理,oldEndVnode 与 newStartVnode 符合 sameVnode 时,将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。同样的,oldEndIdx 向前移动一位,newStartIdx 向后移动一位。
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}

3.6 查找 map
最后是当以上情况都不符合的时候,这种情况怎么处理呢?
else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
通过 createKeyToOldIdx 产生 key 与 index 索引对应的一个 map 表:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
比如说:
[
{xx: xx, key: 'key0'},
{xx: xx, key: 'key1'},
{xx: xx, key: 'key2'}
]
在经过 createKeyToOldIdx 转化以后会变成:
{
key0: 0,
key1: 1,
key2: 2
}
我们可以根据某一个 key 的值,快速地从 oldKeyToIdx 这个 map 中获取相同 key 的节点的索引 idxInOld,然后找到相同的节点。
如果没有 key 值则调用 findIdxInOld 从 oldCh 找到相同 vnode,findIdxInOld 函数定义如下:
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
否则如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined,同时将 vnodeToMove.elm 插入到 oldStartVnode.elm 的前面。同理,newStartIdx 往后移动一位。

如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。

3.7 新节点比旧节点多
当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的,需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}

3.8 老节点比新节点多
同理,如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多,将这些无用的老节点通过 removeVnodes 批量删除即可。
else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

总结
