- 本文主要对最近学习的diff算法做个总结,会对node节点存在key时的diff算法和节点key不存在时的diff算法进行浅析,节点key存在时的最大索引法,双端比较法,最长递增子序列法进行逐一学习;会结合部分源码函数进行代码分析和执行分析;
几个名词概念:
- 渲染器的作用:将虚拟dom渲染为特定平台上的真实元素,在浏览器平台上,渲染器会把虚拟dom渲染为真实dom元素;
- 挂载:渲染器将虚拟dom节点渲染为真实dom节点的过程;
- 打补丁:对于新的元素(即第一次挂载)渲染器会把它挂载到容器内;对于新旧vnode都存在的情况,渲染器则会执行打补丁的操作,即对比新旧vnode,只更新变化的内容;
patch函数的介绍;
- 用途:用于打补丁操作,比对两个新旧虚拟节点;
1. 新旧VNode的比对过程:
- 1.如果vnode节点类型不同,则先将旧的vnode卸载,在挂载新vnode;
- 2.如果节点类型相同,则根据vnode的类型(标签,文本,Fragment片段)调用与之相符的比对函数;
- 2.1 同为标签元素(vnode.type都是字符串),但是新旧节点是不同的标签,即vnode.type的属性值不同(比如旧vnode为p标签,新node为input标签);则直接使用新的标签元素替换旧的标签元素;
- 2.2 同为标签元素,且新旧节点VNode是相同的标签(即vnode.type的属性值相同);则两个vnode之间的差异只会出在props(用于描述真实dom上的属性和属性值)和children上(该节点下的子节点内容),则只需要比对props对象和children并进行更新;
2 更新vnode节点属性props(包括节点上绑定的事件);
- 如果新vnode.props存在,遍历新props对象中的key,将每个key值更新为最新值(key值包含style,class,节点属性Attr),移除旧节点上绑定的事件,添加新节点上的事件;
3.更新vnode的子节点children;
- 注意:实际上在整个新旧
children的比对中,只有当新旧子节点都是多个子节点时才有必要进行真正的核心diff比对,从而尽可能的复用子节点。 - 可能的情况有9种;即新旧节点可能为0个,1个,多个;组合起来就是9种情况;
- 1)旧子节点为0个; 如果新子节点为0个,则什么都不做; 如果新子节点为1个或多个,则新建新子节点并挂载;
- 2)旧子节点为1个时; 如果新子节点为0个;则移除旧子节点; 如果新子节点为1个,则移除旧子节点,新建新子节点并挂载; 如果新子节点为多个,则移除旧子节点,新建每个新子节点进行挂载;
- 3)旧节点为多个时; 如果新子节点为0个,则移除每个旧子节点; 如果新子节点为1个,则移除每个旧子节点,新建新子节点并挂载; 如果新子节点为多个,则调用patchChildren()函数进行diff算法对比,尽可能多的复用旧子节点;
diff算法介绍;
- 用途:在patch函数打补丁对比新旧vnode节点时,如果新旧vnode节点的子节点都是数组类型,则调用diff算法;
1. 节点没有key值时所采用的算法(Vue3中在节点没有key的情况下采用的算法);
- 1.1 遍历新旧节点数组中长度较小的节点数组,应用patch函数进行更新;
- 1.2 对比新旧节点数组的长度,如果newNodeChild.length > oldNodeChild.length, 则说明有新节点需要添加;如果newNodeChild.length < oldNodeChild.length,则说明有旧节点需要移除;
<!-- prevChildren:旧节点数组;nextChildren:新节点数组;-->
// 获取公共长度,取新旧 children 长度较小的那一个
const prevLen = prevChildren.length;
const nextLen = nextChildren.length;
const commonLength = prevLen > nextLen ? nextLen : prevLen;
for (let i = 0; i < commonLength; i++) {
patch(prevChildren[i], nextChildren[i], container);
}
// 如果 nextLen > prevLen,将多出来的新元素添加
if (nextLen > prevLen) {
for (let i = commonLength; i < nextLen; i++) {
mount(nextChildren[i], container);
}
} else if (prevLen > nextLen) {
// 如果 prevLen > nextLen,将多出来的旧元素移除
for (let i = commonLength; i < prevLen; i++) {
container.removeChild(prevChildren[i].el);
}
}
2.节点存在key时的diff算法
- 通过key属性,尽可能多的复用已有的DOM元素, 注意:
- 新增,删除,移动的操作都是对于真实dom来操作的;
- 真实dom的移动顺序要和新vnode的顺序保持一致;
- 不要用数组下标作为key值;否则对节点进行逆序增加,逆序删除等破坏结构顺序的操作时,会产生真实dom没必要的更新,影响渲染效率;当结构中存在输入类的dom时,可能会产生界面显示问题;
- 不要用数组下标index拼接其他值作为key,这样会导致新节点中的每个key都不能在旧节点中找到可复用的节点,每个新节点都需要新建;
2.1. 最大索引法;通过节点key值尽可能多的复用DOM元素;React所采用的;
思路:先外层循环遍历新节点数组,内层循环遍历旧节点数组,根据新旧虚拟节点的key是否相等,判断该新节点是否可以复用旧节点;如果可复用的话是否需要移动以及怎样移动?(复用或新增)然后再循环旧节点数组,查找需要删除的旧节点;
1)遍历新节点数组,找到该新节点在旧节点数组中的下标,并记录下来,maxIndex默认取0;
- 如果找到的下标是递增的,则说明新旧节点数组的顺序一致,无需移动;
- 如果找到的下标 < 最大下标,即递减了,则说明该节点在旧节点数组中比较靠前,但是在新节点数组中靠后,则将该旧节点对应的真实dom移动到上一个新节点对应的真实dom后面;
- 新增:如果该新节点在旧节点数组中未找到,则创建新dom元素,挂载新节点;
2)删除:遍历旧节点数组,判断该旧节点是否在新节点数组中存在,如果不存在,则删除该旧节点对应的真实dom;
<!-- nextChildren:新节点数组;prevChildren:旧节点数组 -->
let lastIndex = 0; // 存放新节点在旧节点数组中的最大下标值
// 以新节点数组为基准遍历
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i];
let j = 0,
find = false; // 标识该新节点是否在旧节点数组中存在;
for (j; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j];
if (nextVNode.key === prevVNode.key) {
find = true;
patch(prevVNode, nextVNode, container);
if (j < lastIndex) {
// 需要移动,将旧节点对应的真实dom移动到上一个新节点对应的真实dom后面(即兄弟节点前面)
const refNode = nextChildren[i - 1].el.nextSibling; // 上一个新节点真实dom的兄弟节点
container.insertBefore(prevVNode.el, refNode);
break;
} else {
// 更新 lastIndex
lastIndex = j;
}
}
}
if (!find) {
// 未在旧节点中找到该新节点,挂载新节点
// 找到 refNode
const refNode =
i - 1 < 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling;
mount(nextVNode, container, false, refNode);
}
}
// 移除已经不存在的节点
for (let i = 0; i < prevChildren.length; i++) {
const prevVNode = prevChildren[i];
// 拿着旧 VNode 去新 children 中寻找是否存在相同的节点
const has = nextChildren.find((nextVNode) => nextVNode.key === prevVNode.key);
if (!has) {
// 如果没有找到相同的节点,则移除该旧节点对应的真实dom
container.removeChild(prevVNode.el);
}
}
2.2.双端比较法; Vue2采用的算法;
- 1)先比较头尾的节点是否相同,如果相同,则直接patchVnode()
- a. oldStartNode 和 newStartNode 对比;
- b. oldEndNode 和 newEndNode 对比;
- c. oldStartNode 和 newEndNode 对比;
- d. oldEndNode 和 newStartNode 对比;
- 2)如果第一步比较的节点不相同,则在剩余旧节点中查找是否存在与新节点相同的节点,如果存在则将旧节点该位置的真实dom进行移动,并将该旧虚拟节点置为undefined,下一次循环时需跳过该位置;如果不存在,则需要新建节点并插入到适当的位置;
<!--
Vue2中源码路径:Vue2\src\core\vdom\patch.js
-->
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧节点数组的开始下标
let newStartIdx = 0 //新节点数组的开始下标
let oldEndIdx = oldCh.length - 1 // 旧节点数组的结束下标
let newEndIdx = newCh.length - 1 // 新节点数组的结束下标
let oldStartVnode = oldCh[0] // 旧节点数组的开始节点
let oldEndVnode = oldCh[oldEndIdx] // 旧节点数组的结束节点
let newStartVnode = newCh[0] // 新节点数组的开始节点
let newEndVnode = newCh[newEndIdx] // 新节点数组的结束节点
/**
* oldKeyToIdx: 对象,旧节点的map结构, {key:index}
* idxInOld: number | undefined,该新节点如果在旧节点中存在时返回的存在下标;
*/
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
/**
注意:真实dom的移动顺序要和新vnode的顺序保持一致;
*/
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 处理前面循环在非理想情况下置为undefined的位置;
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 处理前面循环在非理想情况下置为undefined的位置;
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 旧start节点和新start节点相同,则vnode不移动
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 旧end节点和新end节点相同,则vnode不移动
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 旧start节点和新end节点相同, 则将旧节点对应的真实dom移动到最后面(即最后一个旧节点的下一个兄弟节点前面),
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// nodeOps.nextSibling(oldEndVnode.elm):返回旧end节点的下一个兄弟节点;
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 旧end节点和新start节点相同,则将旧end节点对应的真实dom移动到最前面(即旧start节点的最前面);
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 非理想情况下;比较的节点不相同;
// 则在旧节点中查找是否存在与新节点key值相同的节点,如果存在则将旧节点该位置的真实dom进行移动,并将该旧虚拟节点置为undefined;下一次循环式需跳过该位置
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 构建map结构{key: node下标},方便查找,只在第一次构建
// 根据newStart节点的key值在旧节点数组中查找是否存在相同key值的节点,如果存在将下标index记录下来;
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
// newStart节点不存在key属性,则直接用节点在旧数组中比对查找,
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
// 说明newStart节点未在旧节点数组中找到,则新建节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在旧节点数组中通过key找到了该newStart节点
vnodeToMove = oldCh[idxInOld] // 新节点在旧节点数组中对应位置的节点
if (sameVnode(vnodeToMove, newStartVnode)) {
// 相同的节点元素,只需移动
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
//将旧节点数组中对应位置处的vnode置为空,当oldStartIndex或oldEndIndex走到该位置时,跳过该节点的比对
oldCh[idxInOld] = undefined
// 将找到的节点对应的真实dom节点移动到oldStart节点的前面
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) {
// 当旧节点已经循环完毕,但是新节点中还存在没有被处理的全新节点,则直接将剩余的新节点逐个插入到oldStart前面;
// 比如oldNode: [a, b, c],newNode:[d,a,b,c]的情况;
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新节点先循环完毕,则将剩余旧节点对应的真实dom移除;
// 比如oldNode:[a,b,c],newNode:[a,c]
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
// 判断两个vnode节点是否相同
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
// 是否为注释节点
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
// 当标签为input时,type属性必须相同
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
2.3. 最长递增子序列法,Vue3采用的算法;
- 1 定义变量i,查找相同的前缀进行patch;
- 2.定义变量e1(旧节点数组的长度-1),e2(新节点数组的长度 - 1),查找相同的后缀进行patch;
- 3.patch完相同的前缀和后缀后,判断i,e1,e2之间的关系;如果i大于e1则说明旧节点数组中所有节点都已经参与patch了,但是新节点数组中的节点还存在为patch的节点,即有节点需要新建;i>e2时,说明有旧节点需要移除;
- 4.如果新旧节点数组中都存在未patch的节点,则通过构建未参与patch的新节点数组的最长递增子序列,查找需要移动的节点以及判断节点怎样移动;查找需要创建的新节点进行新建与挂载;
参考博客: vue3.0 diff算法详解(超详细)
源码路径:packages\runtime-core\src\renderer.ts
// can be all-keyed or mixed;节点存在key时的diff比对算法
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. sync from start;查找相同的前缀,只需一个变量即可
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
// n1和n2的节点类型和key值都相同;
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
// 一发现节点不相同,则立即退出循环
break
}
i++
}
// 2. sync from end;查找相同的后缀,由于新旧节点数组的长度不一定相同,所以需要两个变量
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
e1--
e2--
}
// 3. common sequence + mount;公共序列+挂载
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2;
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
// 说明旧节点数组中的所有节点已经参与前两步的patch了
if (i <= e2) {
// 但是新节点数组中还有剩余节点未参与
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
// 将新节点数组中未参与比较的节点逐个进行新建挂载;
// 未参与的节点下标范围:[i, e2]
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
i++
}
}
}
// 4. common sequence + unmount;公共序列+移除
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
// 说明新节点数组中的所有节点已经参与前两步的patch了
while (i <= e1) {
// 但是旧节点数组中还有剩余;则逐个移除未参与的旧节点;未参与的旧节点范围:[i, e1]
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. unknown sequence;未知序列;i既不大于e1也不大于e2;即新旧节点数组中都还有未参与patch的节点,
// 用最长递增子序列查找需要移动的节点以及怎样移动?
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
// 下面的代码以prevChild=[a,b,c,d,e,f,g],nextChild=[a,b,e,d,c,h,f,g]例子带入理解
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren;
// 构建未参与的剩余新节点的map结构,方便查找;map的key为该节点的key值,value为该节点在新节点数组中的下标
const keyToNewIndexMap: Map<string | number, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present(匹配节点&删除不再存在的节点)
let j
let patched = 0 // 记录已patch的节点数量,当patched > toBePatched时,则说明已经patch完毕,多余节点需要移除;
const toBePatched = e2 - s2 + 1 // 新节点数组中剩余未patch的节点个数
let moved = false // 记录节点是否需要移动
// used to track whether any node has moved(用于跟踪是否有任何节点已移动)
let maxNewIndexSoFar = 0 // 最大索引法
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
// 初始化长度为toBePatched的数组,并将数组元素初始化为0,存放未参与patch的新节点在未参与patch的旧节点数组中的下标
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
// 遍历未参与patch的旧节点数组,
const prevChild = c1[i]
if (patched >= toBePatched) {
// 已更新的节点数 > 剩余未patch的节点数,则卸载旧节点
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex // 存放相同key的新节点在新节点数组中的下标
if (prevChild.key != null) {
// 旧节点存在key
newIndex = keyToNewIndexMap.get(prevChild.key) // 用旧节点的key在新节点对应的map中查找是否存在该key值的节点,存在则返回其所在下标
} else {
// key-less node, try to locate a key-less node of the same type(无密钥节点,尝试定位同一类型的无密钥节点)
for (j = s2; j <= e2; j++) {
// 在未参与patch的剩余新节点中查找是否有相同的节点
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
// 如果找到有新节点对应,则记录该节点在新节点数组中的下标
newIndex = j
break
}
}
}
if (newIndex === undefined) {
// 未找到,则说明该旧节点对应的真实dom需要被移除
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 找到了;更新newIndexToOldIndexMap数组中该节点在旧节点数组中的下标值;
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
patched++
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
// 如果需要移动,则返回最长递增子序列的下标数组;
// 如newIndexToOldIndexMap=[5,4,3,0],则返回[2],表示最长递增子序列是由下标为2的元素构成;
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR // [2]
j = increasingNewIndexSequence.length - 1 // 0
// looping backwards so that we can use last patched node as anchor
// i = toBePatched - 1时,即c2[e2]节点
for (i = toBePatched - 1; i >= 0; i--) { // i: 3,2,1,0
// 有多少新节点未被patch
const nextIndex = s2 + i // 新节点的下标
const nextChild = c2[nextIndex] as VNode // 新节点,即 h,c,d,e
// l2 = c2.length,即新节点数组的长度,则新节点的下标为[0, l2-1]
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 此新节点在旧节点中不存在,需要新建挂载
// mount new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
} else if (moved) {
// 此新节点在旧节点中存在,且需要移动
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
// 比较的是下标;
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
处理未知序列的示例:以 prevChild: [a b c d e f g],nextChild: [a b e d c h f g]为例,分析Vue3的diff算法执行;
-
已知条件:c1 = prevChild; c2 = nextChild; i=0;l2=c2.length = 8; l1 = c1.length=7; e1 = 6;e2 = 7
-
1.使用变量i, patch新旧节点数组中相同前缀的节点,如果发现节点不同则退出循环,记录此时变量i的值; 相同前缀节点:[a,b];i=2;
-
2.使用变量e1,e2,patch新旧节点数组中相同后缀的节点,如果发现节点不同则退出循环,记录此时e1,e2变量的值; 相同后缀节点:[f,g],此时e1 = 4;e2=5;
-
3.判断变量i是否大于e1并且小于e2(即旧节点数组中的所有节点都已经参与patch了,说明新节点数组中[i,e2]范围的节点需要新建),
-
4.判断变量i是否大于e2并且小于e1,(即新节点数组中的所有节点都已经参与patch了,旧节点数组中[i,e1]范围的节点需要被移除); 此时 i<e1 && i<e2;即新旧节点数组中都还有未参与patch的节点;
-
5.处理未知序列,即i既不大于e1也不大于e2的情况; s1 = s2 = i = 2
-
5.1 构建未参与patch的剩余新节点的map结构;keyToNewIndexMap = {e:2,d:3,c:4,h:4}
-
5.2 构建剩余未处理的新节点在旧节点数组中的下标;根据下标判断是否有旧节点需要移除;是否有旧节点需要移动? 剩余未处理的新节点个数:toBePatched = e2 - s2 +1 = 4; 剩余未处理的新节点在旧节点数组中的下标:newIndexToOldIndexMap初始为[0,0,0,0];
-
遍历剩余未处理的旧节点[i,e1]范围的节点,判断剩余未处理的新节点在旧节点数组中的下标,从而更新newIndexToOldIndexMap数组;更新后的值为[5,4,3,0]; newIndex:与旧节点的key相同的新节点在keyToNewIndexMap的键值; 代码执行过程如下:
-
最大索引法判断是否有节点需要移动,maxNewIndexSoFar:未参与的新节点在旧节点数组中的最大下标;
-
5.3 根据newIndexToOldIndexMap数组获取最长递增子序列的下标数组;最长递增子序列的算法可以参考leetcode第300题; increasingNewIndexSequence = [2],表示最长递增子序列是由下标为2的元素构成; j = increasingNewIndexSequence.length-1 = 0;
-
5.4 从后往前遍历未patch的新节点;对新增的节点进行新建挂载;如果moved=true,则表明有旧节点需要移动,则对旧节点进行适当的移动操作;
如果文中有错误欢迎大家指正,我们一起学习一起进步,谢谢啦;
全文参考资料:
- <<Vue.js的设计与实现>>
- github.com/HcySunYang/…
- blog.csdn.net/zl_Alien/ar…