建议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
是oldStartVnode
oldEndIdx
:oldCh
的结束指针,对应的vnode
是oldEndVnode
newStartIdx
:ch
的开始指针,对应的vnode
是newStartVnode
newEndIdx
:ch
的结束指针,对应的vnode
是newEndVnode
oldKeyToIdx
是一个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)
}