9-1 减少DOM操作的性能开销
js
// 旧vnode
const oldVNode = {
type: 'div',
children: [
{ type: 'p', children: '1' },
{ type: 'p', children: '2' },
{ type: 'p', children: '3' },
]
}
// 新vnode
const newVNode = {
type: 'div',
children: [
{ type: 'p', children: '4' },
{ type: 'p', children: '5' },
{ type: 'p', children: '6' },
]
}
按照之前的做法,当更新子节点时,需要执行6次DOM操作:
- 卸载所有旧节点,需要3次DOM删除操作;
- 挂载所有新节点,需要3次DOM添加操作。
所以更理想的更新方式呢?
重新实现两组子节点的更新逻辑,如下面patchChildren函数:
js
function patchChildren(n1, n2, container){
if(typeof n2.children === 'string'){
// 省略部分代码
} else if(Array.isArray(n2.children)){
// 新旧children
// 遍历旧的children
// 调用patch函数逐个更新子节点
} else {
// 省略部分代码
}
}
图9-1 仅更新文本子节点
图9-2 卸载已经不存在的节点
图9-3 挂载新的节点
这时修改patchChildren方法:
js
function patchChildren(n1, n2, container){
if(typeof n2.children === 'string'){
// 省略部分代码
} else if(Array.isArray(n2.children)){
// 旧的一组子节点的长度
// 新的一组子节点的长度
// 两组子节点的公共长度commonLength,即两者中较短的那一组子节点的长度
// 遍历commonLength次,调用patch
// 如果newLen > oldLen,说明有新子节点需要挂载
// 如果oldLen > newLen,说明有旧子节点需要卸载
} else {
// 省略部分代码
}
}
9.2 DOM 复用与key的作用
js
// oldChildren
[
{ type: 'p' },
{ type: 'div' },
{ type: 'span' },
]
// newChildren
[
{ type: 'span' },
{ type: 'p' },
{ type: 'div' },
]
观察新旧两组子节点,二者只是顺序不同。我们怎么判断新的一组子节点的第一个子节点{ type: 'span' }
和旧的一组子节点中的第3个子节点相同呢?
是否可用vnode.type的值来判断呢?
是否有其他判断的方式呢?
图9-4 有key与无key
DOM可复用那它还需要更新吗?
js
const oldVNode = { type: 'p', key: 1, children: 'text 1' }
const newVNode = { type: 'p', key: 1, children: 'text 2' }
接着修改patchChildren函数:
js
function patchChildren(n1, n2, container){
if(typeof n2.children === 'string'){
// 省略部分代码
} else if(Array.isArray(n2.children)){
// 遍历新的children
// 遍历旧的children
// 如果找到了具有相同key值的两个节点,说明可复用,但仍然需要调用patch函数更新
} else {
// 省略部分代码
}
}
9-3 找到需要移动的元素
上一节我们能通过key值找到可复用的节点。接下来需要思考,如何判断一个节点是否需要移动,以及如何移动。
图9-5 节点顺序不变
寻找节点过程中,得到位置索引序列是多少?
图9-6 节点顺序变化
寻找节点过程中,得到位置索引序列是多少?
结论是什么?
我们可以用lastIndex变量存储整个寻找过程中遇到的最大索引值。
js
function patchChildren(n1, n2, container){
if(typeof n2.children === 'string'){
// 省略部分代码
} else if(Array.isArray(n2.children)){
// 用lastIndex存储最大索引
// 遍历新子节点
// 遍历旧子节点
// 如果他们的key相同 -> patch
// 如果当前找到的节点在旧children中的索引值 < lastIndex
// 说明该节点对应的真实DOM需要移动
// 如果当前找到的节点在旧children中的索引值 >= lastIndex
// 则更新lastIndex的值
} else {
// 省略部分代码
}
}
9.4 如何移动元素
移动节点指的是,移动一个虚拟节点所对应的真实DOM节点,并不是移动虚拟节点本身。如何获取到真实的DOM节点呢?
回顾一下patchElement函数的代码。
js
function patchElement(n1, n2){
// 新的vnode也引用了真实DOM元素
const el = n2.el = n1.el
// 省略部分代码
}
图9-8 使新的子节点也引用真实DOM元素
它的更新步骤:
1.取新的一组子节点中第一个节点p-3。尝试在旧的一组子节点中找到具有相同key值的可复用节点。所以节点p-3对应的真实DOM需要移动吗?
2.取新的一组子节点中第二个节点p-1。尝试在旧的一组子节点中找到具有相同key值的可复用节点。所以节点p-1对应的真实DOM需要移动吗?
3.取新的一组子节点中第三个节点p-2。尝试在旧的一组子节点中找到具有相同key值的可复用节点。所以节点p-2对应的真实DOM需要移动吗?
请根据图9-8 画出更新之后的图。
接着我们着手实现代码。patchChildren函数的流程图如下:
9-5 添加新元素
图9-12 新增节点p-4 对于新增节点,在更新时我们应该正确地将它挂载,这主要分为两步:
- 想办法找到新增节点;
- 将新增节点挂载到正确位置;
我们如何找到新增节点?
图9-13 新旧两组子节点与真实DOM元素的当前状态
我们先模拟执行简单Diff算法的更新逻辑。 1.取新的一组子节点中第一个节点p-3。在旧的一组子节点中寻找可复用的节点。所以节点p-3对应的真实DOM需要移动吗?
2.取新的一组子节点中第二个节点p-1。在旧的一组子节点中寻找可复用的节点。所以节点p-1对应的真实DOM需要移动吗?
3.取新的一组子节点中第三个节点p-4。在旧的一组子节点中寻找可复用的节点。所以节点p-4对应的真实DOM存在吗?
4.取新的一组子节点中第四个节点p-2。在旧的一组子节点中寻找可复用的节点。所以节点p-2对应的真实DOM需要移动吗?
请画出更新之后的图。
patchChildren函数代码的流程图如下:
现在的patch函数还不支持传递第四个参数,即锚点元素。
// 这个后面补充
9-6 移除不存在的元素
图9-18 新旧两组子节点与真实DOM节点的当前状态
接着,我们开始模拟执行更新的过程。
1.取新的一组子节点中第一个节点p-3。在旧的一组子节点中寻找可复用的节点。所以节点p-3对应的真实DOM需要移动吗?
1.取新的一组子节点中第二个节点p-1。在旧的一组子节点中寻找可复用的节点。所以节点p-1对应的真实DOM需要移动吗?
更新结束之后,我们发现,节点p-2对应的真实DOM仍然存在。所以需要增加额外的逻辑来删除遗留节点。