二、组件更新
副作用渲染函数更新组件的过程
componentEffect函数,主要做了三件事
- 更新组件vnode节点,这里判断组件实例是否有新的组件vnode(用next表示),有则更新组件vnode,没有则next指向之前的组件vnode,这里有个组件更新的策略的逻辑
- 渲染新的子树 vnode
- 根据新旧子树 vnode 执行 patch 逻辑
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件
} else {
// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树 vnode
instance.subTree = nextTree
// 组件更新核心逻辑,根据新旧子树 vnode 做 patch
patch(prevTree, nextTree,
// 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
hostParentNode(prevTree.el),
// 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG)
// 缓存更新后的 DOM 节点
next.el = nextTree.el
}
}, prodEffectOptions)
}
核心逻辑:patch流程
- 比较新旧节点是否是相同的vnode类型,如果不同,直接删掉旧节点,挂载新节点
- 如果是相同的vnode类型,就需要走diff更新流程了,这里我们只分析普通元素类型和组件类型的处理过程
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
// n1 设置为 null 保证后续都走 mount 逻辑
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT
} else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
function isSameVNodeType (n1, n2) {
// n1 和 n2 节点的 type 和 key 都相同,才是相同节点
return n1.type === n2.type && n1.key === n2.key
}
1. 处理组件
父组件App如下
<template>
<div class="app">
<p>This is an app.</p>
<hello :msg="msg"></hello>
<button @click="toggle">Toggle msg</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: 'Vue'
}
},
methods: {
toggle() {
this.msg = this.msg ==== 'Vue'? 'World': 'Vue'
}
}
}
</script>
子组件Hello如下
<template>
<div class="hello">
<p>Hello, {{msg}}</p>
</div>
</template>
<script>
export default {
props: {
msg: String
}
}
</script>
- 点击父组件的按钮触发toggle函数,修改data里的msg,触发App组件的重新渲染
- 这里App的根节点是div,是一个普通元素的vnode,走processElement函数逻辑
- 深度优先遍历,更新完当前节点后,遍历当前节点的子节点,遇到hello组件,走processComponent函数逻辑
updateComponent的主要逻辑
- 先执行 shouldUpdateComponent 函数,根据新旧子组件 vnode 来判断是否需要更新子组件。 shouldUpdateComponent 函数的内部,主要是通过检测和对比组件 vnode 中的 props、children、dirs、transiton 等属性,来决定子组件是否需要更新。
- 执行invalidateJob,避免子组件由于自身数据变化导致的重复更新
- 然后又执行了子组件的副作用渲染函数 instance.update 来主动触发子组件的更新。
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 挂载组件
} else {
// 更新子组件
updateComponent(n1, n2, parentComponent, optimized)
}
}
const updateComponent = (n1, n2, parentComponent, optimized) => {
const instance = (n2.component = n1.component)
// 根据新旧子组件 vnode 判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数
instance.update()
} else {
// 不需要更新,只复制属性
n2.component = n1.component
n2.el = n1.el
}
}
再回到副作用渲染函数中
- 更新组件的dom前,更新组件vnode节点信息,包括更改组件实例的vnode指针,更新props,更新插槽等,因为组件在稍后执行 renderComponentRoot 时会重新渲染新的子树 vnode ,它依赖了更新后的组件 vnode 中的 props 和 slots 等数据。
- 组件重新渲染可能会有两种场景
- 一种是组件本身的数据变化,这种情况下 next 是 null;
- 另一种是父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下 next 就是新的子组件 vnode。
- 那子组件对应的新的vnode是什么时候创建的?
- 在父组件重新渲染的过程,通过renderComponentRoot渲染子树vnode的时候生成,因为子树 vnode 是个树形结构,通过遍历它的子节点就可以访问到其对应的组件 vnode。
- 再拿我们前面举的例子说,当 App 组件重新渲染的时候,在执行 renderComponentRoot 生成子树 vnode 的过程中,也生成了 hello 组件对应的新的组件 vnode。
- 所以 processComponent 处理组件 vnode,本质上就是去判断子组件是否需要更新,如果需要则递归执行子组件的副作用渲染函数来更新,否则仅仅更新一些 vnode 的属性,并让子组件实例保留对组件 vnode 的引用,用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件 vnode。
// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
const updateComponentPreRender = (instance, nextVNode, optimized) => {
// 新组件 vnode 的 component 属性指向组件实例
nextVNode.component = instance
// 旧组件 vnode 的 props 属性
const prevProps = instance.vnode.props
// 组件实例的 vnode 属性指向新的组件 vnode
instance.vnode = nextVNode
// 清空 next 属性,为了下一次重新渲染准备
instance.next = null
// 更新 props
updateProps(instance, nextVNode.props, prevProps, optimized)
// 更新 插槽
updateSlots(instance, nextVNode.children)
}
2. 处理普通元素
前面也说过,组件是抽象的,组件的更新最终还是会落到对普通 DOM 元素的更新。所以接下来我们详细分析一下组件更新中对普通元素的处理流程。
<template>
<div class="app">
<p>This is {{msg}}.</p>
<button @click="toggle">Toggle msg</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: 'Vue'
}
},
methods: {
toggle() {
this.msg = 'Vue'? 'World': 'Vue'
}
}
}
</script>
- 当点击按钮会执行toggle函数,修改data中的msg,触发了App组件的重新渲染,重新渲染子树的vnode节点是一个普通元素的vnode,所以应该走processElement逻辑
- processElement做了两件事
- 更新props,这里的 patchProps 函数就是在更新 DOM 节点的 class、style、event 以及其它的一些 DOM 属性
- 更新子节点,由patchChildren函数实现
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
// 挂载元素
} else {
// 更新元素
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => {
const el = (n2.el = n1.el)
const oldProps = (n1 && n1.props) || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 更新 props
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 更新子节点
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}
patchChildren函数
对于一个元素的子节点vnode有三种情况:纯文本、vnode 数组和空,那排列组合对于新旧子节点来说就有9种情况
- 旧子节点是纯文本
- 如果新子节点也是纯文本,那么做简单地文本替换即可;
- 如果新子节点是空,那么删除旧子节点即可;
- 如果新子节点是 vnode 数组,那么先把旧子节点的文本清空,再去旧子节点的父容器下添加多个新子节点。
- 旧子节点是空
- 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
- 如果新子节点也是空,那么什么都不需要做;
- 如果新子节点是 vnode 数组,那么直接去旧子节点的父容器下添加多个新子节点即可。
- 旧子节点是 vnode 数组
- 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点;
- 如果新子节点是空,那么删除旧子节点即可;
- 如果新子节点也是 vnode 数组,那么就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。
const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { shapeFlag } = n2
// 子节点有 3 种可能情况:文本、数组、空
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
// 数组 -> 文本,则删除之前的子节点
unmountChildren(c1, parentComponent, parentSuspense)
}
if (c2 !== c1) {
// 文本对比不同,则替换为新文本
hostSetElementText(container, c2)
}
} else {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
// 之前的子节点是数组
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 新的子节点仍然是数组,则做完整地 diff
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
// 数组 -> 空,则仅仅删除之前的子节点
unmountChildren(c1, parentComponent, parentSuspense, true)
}
} else {
// 之前的子节点是文本节点或者为空
// 新的子节点是数组或者为空
if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
// 如果之前子节点是文本,则把它清空
hostSetElementText(container, '')
}
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 如果新的子节点是数组,则挂载新子节点
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
}
}
核心diff算法
新子节点数组相对于旧子节点数组的变化,无非是通过更新、删除、添加和移动节点来完成,而核心 diff 算法,就是在已知旧子节点的 DOM 结构、vnode 和新子节点的 vnode 情况下,以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作。
1. 同步头部节点
从头部开始,同步比较新节点和旧节点,如果相同,则执行patch更新节点,如果不同或者索引大于e1或者e2,同步过程结束
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
// 相同的节点,递归执行 patch 更新节点
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
break
}
i++
}
}
可以看到,完成头部节点同步后:i 是 2,e1 是 3,e2 是 4。
2. 同步尾部节点
从尾部开始,同步比较新节点和旧节点,如果相同,则执行patch更新节点,如果不同或者索引i大于e1或者e2,则同步过程结束
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
break
}
e1--
e2--
}
}
可以看到,完成尾部节点同步后:i 是 2,e1 是 1,e2 是 2。
接下来,只有三种情况要处理
- 新子节点有剩余要添加的新节点
- 旧子节点有剩余要删除的多余节点
- 未知子序列
3. 添加新的节点
如果索引i大于尾部索引e1且小于e2,那么从索引i开始到索引e2之间,我们直接挂载这部分的节点到新子树上
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
// 3. 挂载剩余的新节点
// i = 2, e1 = 1, e2 = 2
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
while (i <= e2) {
// 挂载新节点
patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG)
i++
}
}
}
}
4. 删除多余节点
如果索引i大于尾部索引e2,那么从索引i到索引e1之间,我们直接删除旧子树的这部分节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 4, e2 = 3
// (a b) c d e
// (a b) d e
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 4, e2 = 3
// (a b) c (d e)
// (a b) (d e)
// 3. 普通序列挂载剩余的新节点
// i = 2, e1 = 2, e2 = 1
// 不满足
if (i > e1) {
}
// 4. 普通序列删除多余的旧节点
// i = 2, e1 = 2, e2 = 1
else if (i > e2) {
while (i <= e1) {
// 删除节点
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
}
5. 移动子节点——处理未知子序列
例如两个列表
a,b,c,d,e,f,g,h
a,b,e,c,d,i,g,h
首先同步头节点, a和b,i为2,e1为7,e2为7
c,d,e,f,g,h
e,c,d,i,g,h
然后同步尾节点,g和h,i为2,e1为5,e2为5
c,d,e,f
e,c,d,i
对于未知子序列,我们需要
- 在新旧子节点序列中找到相同节点并更新
- 找到多余节点删除
- 剩余需要移动的节点
查找过程需要对比新旧子序列,是On^2的时间复杂度,为了优化,空间换时间,建立索引图,把时间复杂度降低到On
最后,这种情况需要移动子节点
核心算法是,寻找旧节点的最长递增子序列,然后在新节点中移动所有不在递增序列中的元素即可
a. 建立索引图
v-for列表中为每一项分配的key作为唯一id
我们以s1和s2分别作为新旧子序列的开始索引,建立一个keyToNewIndexMap的Map结构
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 7, e2 = 7
// (a b) c d e f g h
// (a b) e c d i g h
// 2. 从尾部开始同步
// i = 2, e1 = 7, e2 = 7
// (a b) c d e f (g h)
// (a b) e c d i (g h)
// 3. 普通序列挂载剩余的新节点, 不满足
// 4. 普通序列删除多余的旧节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子序列开始索引,从 i 开始记录
const s1 = i
// 新子序列开始索引,从 i 开始记录
const s2 = i
// 5.1 根据 key 建立新子序列的索引图
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = c2[i]
keyToNewIndexMap.set(nextChild.key, i)
}
}
最终我们得到{e: 2, c: 3, d: 4, i: 5}的索引图
b. 更新和移除旧节点
newIndexToOldIndexMap数组用来存储新子序列节点的索引和旧子序列节点的索引之间的映射关系,用于确定最长递增子序列,这个数组的长度为新子序列的长度,每个元素的初始值设为0,如果遍历旧子序列后仍有元素的值为0,则说明该节点是新添加的
具体的处理过程:
- 正序遍历旧子序列,根据前面的索引图查找旧子序列中的节点在新子序列中的索引
- 如果找不到,说明新子序列中没有该节点,删掉
- 如果找得到,将旧子序列中的索引更新到newIndexToOldIndexMap中,这里加了1的偏移量,为了应对i为0的特殊情况
- maxNewIndexSoFar始终存储上次求值的newIndex,一旦最新的newIndex小于maxNewIndexSoFar,说明遍历旧子序列的节点在新子序列中的索引并不是一直递增的,存在移动的情况
- 更新新旧子序列中匹配的节点,如果所有新的子序列节点都已经更新,而旧子序列遍历还未结束,说明剩余的节点就是多余的,删掉即可
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 7, e2 = 7
// (a b) c d e f g h
// (a b) e c d i g h
// 2. 从尾部开始同步
// i = 2, e1 = 7, e2 = 7
// (a b) c d e f (g h)
// (a b) e c d i (g h)
// 3. 普通序列挂载剩余的新节点,不满足
// 4. 普通序列删除多余的旧节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子序列开始索引,从 i 开始记录
const s1 = i
// 新子序列开始索引,从 i 开始记录
const s2 = i
// 5.1 根据 key 建立新子序列的索引图
// 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
// 新子序列已更新节点的数量
let patched = 0
// 新子序列待更新节点的数量,等于新子序列的长度
const toBePatched = e2 - s2 + 1
// 是否存在要移动的节点
let moved = false
// 用于跟踪判断是否有节点移动
let maxNewIndexSoFar = 0
// 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长递增子序列
const newIndexToOldIndexMap = new Array(toBePatched)
// 初始化数组,每个元素的值都是 0
// 0 是一个特殊的值,如果遍历完了仍有元素的值为 0,则说明这个新节点没有对应的旧节点
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0
// 正序遍历旧子序列
for (i = s1; i <= e1; i++) {
// 拿到每一个旧子序列节点
const prevChild = c1[i]
if (patched >= toBePatched) {
// 所有新的子序列节点都已经更新,剩余的节点删除
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
// 查找旧子序列中的节点在新子序列中的索引
let newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 找不到说明旧子序列已经不存在于新子序列中,则删除该节点
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况,影响对后续最长递增子序列的求解
newIndexToOldIndexMap[newIndex - s2] = i + 1
// maxNewIndexSoFar 始终存储的是上次求值的 newIndex,如果不是一直递增,则说明有移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新新旧子序列中匹配的节点
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
patched++
}
}
}
至此,我们完成了新旧子序列节点的更新,多余旧节点的删除,并建立了一个newIndexToOldIndexMap存储新子序列节点的索引和旧子序列索引之间的映射关系,并确定是否移动
c. 移动和挂载新节点
- 我们采取倒序的方式遍历新子序列,因为倒序遍历可以方便我们使用最后更新的节点作为锚点,判断 newIndexToOldIndexMap[i] 是否为 0
- 如果是0,为新节点,挂载
- 否则,如果是moved为true,存在节点移动
- 看节点的索引是不是在最长递增子序列中
- 如果在,则继续倒序遍历新子序列
- 如果不在,移动到锚点的前面
- 看节点的索引是不是在最长递增子序列中
pre children: (a,b),c,d,e,f,(g,h)
next children: (a,b),e,c,d,i,(g h)
初始状态
1. toBePatched = 4
2. 待更新的长度为4,j = 1
3. 最长递增子序列increasingNewIndexSequence的值为[1,2]
4. 新子序列节点索引和旧子序列节点索引之间的映射为newIndexToOldIndexMap [5,3,4,0]
倒序遍历新子序列
1. 索引i=3,j=1,遍历到i节点,newIndexToOldIndexMap[3]=0,说明是新节点,挂载它
2. 索引i=2,j=1,遍历到d节点,newIndexToOldIndexMap[2]=4,increasingNewIndexSequence[1]=2,在最长递增子序列中,j--
3. 索引i=1,j=0,遍历到c节点,newIndexToOldIndexMap[1]=3,increasingNewIndexSequence[1]=1,在最长递增子序列中,j--
4. 索引i=0,j=-1,遍历到e节点,j小于0,且不在最长递增子序列中,做一次移动操作,把e插入到上一个更新的节点之前,也就是c的前面
5. 新子序列倒序完成了新节点的插入和旧节点的移动操作,完成了整个diff算法
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 6, e2 = 7
// (a b) c d e f g
// (a b) e c d h f g
// 2. 从尾部开始同步
// i = 2, e1 = 6, e2 = 7
// (a b) c (d e)
// (a b) (d e)
// 3. 普通序列挂载剩余的新节点, 不满足
// 4. 普通序列删除多余的节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子节点开始索引,从 i 开始记录
const s1 = i
// 新子节点开始索引,从 i 开始记录
const s2 = i //
// 5.1 根据 key 建立新子序列的索引图
// 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
// 5.3 移动和挂载新节点
// 仅当节点移动时生成最长递增子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
let j = increasingNewIndexSequence.length - 1
// 倒序遍历以便我们可以使用最后更新的节点作为锚点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
// 锚点指向上一个更新的节点,如果 nextIndex 超过新子节点的长度,则指向 parentAnchor
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 挂载新的子节点
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG)
} else if (moved) {
// 没有最长递增子序列(reverse 的场景)或者当前的节点索引不在最长递增子序列中,需要移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2)
} else {
// 倒序递增子序列
j--
}
}
}
}
d. 最长递增子序列
思路:
这种做法的主要目的是要使上升子序列的长度尽可能的长,序列的差尽可能的小,,就要使序列上升的速度尽可能的慢,让序列内末尾数字尽可能的小。从而可以获得更长的递增子序列,这是一种贪心算法的思想
维护一个result数组,用来存放长度为 i 的递增子序列最小末尾值的索引,并不是最长递增子序列, 额外用一个数组p存储每次更新result前最后一个索引的值,建立映射关系 遍历数组
- 如果
arr[i] > arr[result[result.length - 1]],更新p的映射关系p[i] = j, 把i push到result尾部 - 否则,在result数组中二分查找,找到第一个比
arr[i]大的arr[result[u]],更新p的映射关系p[i]=result[u-1],替换result[u] = i
回溯result,更新为正确的索引,result最后一个值肯定是正确的最大的值,从最后一个值倒序遍历, u = result.length, v = result[u - 1],该值是正确的
- 从p中找到上一个更新的值,并更新result
function getSequence (arr) {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
// 存储在 result 更新前的最后一个索引的值
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
// 二分搜索,查找比 arrI 小的节点,更新 result 的值
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
// 回溯数组 p,找到最终的索引
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}