总结
1. 什么是 Diff
用来比较两个虚拟 DOM 的差异,只更新需要更改的部分,可以显著提高页面更新的性能。
2. key 的作用
作用:找到可复用DOM
可复用也需要判断是否需要更新内容
3. 如何找到移动的节点
总思路:当新旧两组子节点的节点顺序不变时,就不需要额外的移动操作。
实现思路:每一次寻找可复用的节点时,都会记录该可复用节点在旧的一组子节点中的位置索引。如果把这些位置索引值按照先后顺序排列,则可以得到一个序列,如果该序列提增, 则无需移动。
新节点在旧节点寻找具有相同 key
值节点的过程中,遇到的最大索引值, 记录在 lastIndex
。如果在后续寻找的过程中,存在【索引值比当前遇到的最大索引值 lastIndex
】还要小的节点,则意味着该节点需要移动。(当然这种方法有弊端,后续会改进)
4. 如何移动节点
已知如何找到待移动的节点,下一步便是移动节点
节点的真实 DOM 存在 vnode.el
中,在移动前会对新节点打内容补丁(因为新节点没有 vnode.el
属性)
js
function patchElement(oldNode, newNode) {
// 新的 vnode 也引用了真实 DOM 元素
const el = newNode.el = oldNode.el
// 省略部分代码
}
通过上面函数,新节点就有对应的真实 DOM
了。如下图
到这一步, 仅仅在内存上的 js
对象有对应真实 DOM
,但还未进行移动。对应关系如下
找到节点和移动节点是同时进行的
通过 key
找到可复用节点,查找和移动同时进行,利用 lastIndex
判断是否需要移动。
lastIndex
为 0,p-3
旧节点索引 2, 2 > 0 不需要移动。lastIndex
更新为 2。lastIndex
为 2,p-1
旧节点索引 0, 0 < 2 需要移动,根据新节点的顺序,需要移动到p-3
后面。lastIndex
依然为 2。lastIndex
为 2,p-2
旧节点索引 1, 1 < 2 需要移动,根据新节点的顺序,需要移动到p-1
后面。lastIndex
依然为 2。
以上步骤操作如图
具体代码入下
js
function patchChildren(n1, n2, container) {
if (typeof n2.children === 'string') {
// 省略部分代码
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let j = 0
for (j; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 代码运行到这里,说明 newVNode 对应的真实 DOM 需要移动
// 先获取 newVNode 的前一个 vnode,即 prevVNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,它不需要移动
if (prevVNode) {
// 由于我们要将 newVNode 对应的真实 DOM 移动到
// prevVNode 所对应真实 DOM 后面,
// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
const anchor = prevVNode.el.nextSibling
// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
// 也就是 prevVNode 对应真实 DOM 的后面
insert(newVNode.el, container, anchor)
}
} else {
lastIndex = j
}
break
}
}
}
} else {
// 省略部分代码
}
}
新旧用短的为基准去做比较
- 比较key和节点类型,相同则比较内容,为内容打补丁。
- 先为内容打补丁,再进行移动