背景补充
React的列表渲染元素常被要求添加key属性作为元素的唯一标识,它可以告诉React哪些元素发生了修改或删除,而又有哪些元素可以复用,以达到最小化DOM操作的目的。 key属性并非需要全局唯一,只需要在同一父节点下的兄弟节点之间唯一即可,这也侧面说明了key属性是用来识别这种 同一父节点下的类型相同的子节点。
整体思路
主要的关注点是 ==当前要渲染的节点是否是新节点,是否可以复用== 和 ==当前元素的位置是否发生变化== 在diff virtualDOM 和 oldDOM 时,若类型相同,可以通过遍历 oldDOM.childNodes 筛选出原先带key的节点,并存储起来。 在接下来遍历virtualDOM.children时,若子节点存在key属性,则使用这个key去上面的缓存里查找,若能够找到,说明是节点已经存在,只需要进行位置的判断和调整。若找不到,说明是一个新节点,需要新建。
代码实现
// diff.js
// 将oldDOM中带有key属性的子节点存储在一个缓存对象里
const keyedElements = {};
for (let i = 0; i < oldDOM.childNodes.length; i++) {
const oldChildNode = oldDOM.childNodes[i];
if (oldChildNode.nodeType === 1) {
let key = oldChildNode.getAttribute('key');
if (key) {
keyedElements[key] = oldChildNode;
}
}
}
const hasNoKey = Object.keys(keyedElements).length === 0;
if (hasNoKey) {
// 元素节点-更新属性
virtualDOM.children.forEach((child, index) => {
// 暂时通过index来获取child对应的oldDom
diff(child, oldDOM, oldDOM.childNodes[index]);
});
} else {
// 循环 virtualDOM,通过子节点的key,查找老的子节点
virtualDOM.children.forEach((child, index) => {
const key = child.props.key;
if (key) {
const targetNode = keyedElements[key];
if (targetNode) {
// 位置判断
if (
oldDOM.childNodes[index] &&
oldDOM.childNodes[index] !== targetNode
) {
oldDOM.insertBefore(targetNode, oldDOM.childNodes[index]);
}
}
}
});
}