React的diff算法

- 只对同级节点进行对比,如果DOM节点跨层级移动,则不复用
- 用key来构建一个老节点的map,复用一个后要从map里删除
lastPlacedIndex
表示最后一个不需要移动的节点的索引
- 移动时的原则是尽量少量的移动,如果必须有一个要动,新地位高的不动,新地位低的动
对比顺序
- 1、如果可以找到key对应的节点,再对比类型,如果类型不同,就删除旧节点重新创建,
- 2、类型相同,对比lastPlacedIndex 与 oldIndex,lastPlacedIndex <= oldIndex 不需要移动,否则就需要移动位置,并且更新属性
将A B C D E F修改为A C E B G 的执行顺序
- lastPlacedIndex = 0
- A在map里面存在,而且位置相同,复用节点更新属性
- C 对比 lastPlacedIndex < oldIndex,lastPlacedIndex = 2,位置不动,只更新属性
- E 对比 lastPlacedIndex < oldIndex,lastPlacedIndex = 4,位置不动,只更新属性
- B 对比 lastPlacedIndex > oldIndex,需要移动位置并更新属性
- G 在map里找不到,需要创建并插入
- 将map中剩余的元素 D F标记为删除
修改dom的顺序: 先删除,然后更新与移动,最后做插入操作
const EFFECT_TYPE = {
UPDATE: 2,
INSERT: 4,
INSERT_UPDATE: 6,
DELETE: 8,
};
const oldNodes = [{ key: "A" }, { key: "B" }, { key: "C" }, { key: "D" }];
const newNodes = [
{ key: "D" },
{ key: "A" },
{ key: "F" },
{ key: "G" },
{ key: "B" },
];
function diff(oldNodes, newNodes) {
let lastPlacedIndex = 0;
const oldNodeMap = oldNodes.reduce((memo, v, i) => {
memo[v.key] = v;
v.oldIndex = i;
return memo;
}, {});
newNodes.forEach((newNode, newIndex) => {
const oldNode = oldNodeMap[newNode.key];
if (!oldNode) {
newNode.effectTag = EFFECT_TYPE.INSERT;
newNode.insertIndex = newIndex;
return;
}
delete oldNodeMap[newNode.key];
if (lastPlacedIndex <= oldNode.oldIndex) {
newNode.effectTag = EFFECT_TYPE.UPDATE;
lastPlacedIndex = oldNode.oldIndex;
} else {
newNode.effectTag = EFFECT_TYPE.INSERT_UPDATE;
}
});
return Object.keys(oldNodeMap);
}
const deletions = diff(oldNodes, newNodes);
console.log("删除节点", deletions);
console.log(newNodes);
Vue的diff算法
vue中的diff算法涉及到操作dom的逻辑,所以用html来做演示
- oldNodes表示老的虚拟节点列表,el-真实dom,tag-标签类型
- domDiff方法接收三个参数,el-要参与对比节点的父节点,oldChildren-老的虚拟dom列表,newChildren-新的虚拟dom列表
- vue中的节点对比采用双指针,从两端向中间遍历,当指针交叉的时候,就是对比完成了
- 开始遍历时,首先依次进行头头、尾尾、头尾、尾头对比,这也是vue中diff算法 的一个优化点
- 都对比完了,再对比其他没有移动规律的节点


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul id="container">
<li id="domA">A</li>
<li id="domB">B</li>
<li id="domC">C</li>
<li id="domD">D</li>
</ul>
<script>
const oldNodes = [
{ key: "A", el: domA, tag: "li" },
{ key: "B", el: domB, tag: "li" },
{ key: "C", el: domC, tag: "li" },
{ key: "D", el: domD, tag: "li" },
];
const newNodes = [
{ key: "C", tag: "li" },
{ key: "A", tag: "li" },
{ key: "F", tag: "li" },
{ key: "G", tag: "li" },
{ key: "B", tag: "li" },
];
function isSameVnode(newVnode, oldVnode) {
return newVnode.tag === oldVnode.tag && newVnode.key == oldVnode.key;
}
function domDiff(el, oldChildren, newChildren) {
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex];
let oldNodeMap = oldChildren.reduce((memo, item, index) => {
memo[item.key] = index;
return memo;
}, {});
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex];
console.log("1. oldStartVnode 为空");
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex];
console.log("2. oldEndVnode 为空");
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
console.log("3. 头头相同", newStartVnode.key);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
console.log("4. 尾尾相同", newEndVnode.key);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
console.log(
`5. 头尾相同 移动 ${oldStartVnode.key} 到 ${oldEndVnode.key}的下一节点之前`
);
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
console.log(
`6. 尾头相同 移动${oldEndVnode.key} 到${oldStartVnode.key}之前`
);
el.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
} else {
let moveIndex = oldNodeMap[newStartVnode.key];
if (moveIndex === undefined) {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
console.log(`7. 创建新节点${newStartVnode.key} 插入到 ${oldStartVnode.key}之前`);
} else {
let moveVnode = oldChildren[moveIndex];
el.insertBefore(moveVnode.el, oldStartVnode.el);
oldChildren[moveIndex] = undefined;
console.log(
`8. 移动乱序节点${moveVnode.key} 到 ${oldStartVnode.key} 之前`
);
}
newStartVnode = newChildren[++newStartIndex];
}
}
if (newStartIndex <= newEndIndex) {
let anchor =
newChildren[newEndIndex + 1] === null
? null
: newChildren[newEndIndex + 1].el;
for (let i = newStartIndex; i <= newEndIndex; i++) {
console.log("插入", newChildren[i].key);
el.insertBefore(createElm(newChildren[i]), anchor);
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i];
console.log("删除", child.key);
child && el.removeChild(child.el);
}
}
}
function createElm(vnode) {
let { tag, text, key } = vnode;
if (typeof tag === "string") {
vnode.el = document.createElement(tag);
vnode.el.innerText = key;
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
domDiff(container, oldNodes, newNodes);
</script>
</body>
</html>
区别
相同点
- 都是两组虚拟dom的对比(react16.8之后是fiber与虚拟dom的对比)
- 只对同级节点进行对比,简化了算法复杂度
- 都用key做为唯一标识,进行查找,只有key和标签类型相同时才会复用老节点
- 遍历前都会根据老的节点构建一个map,方便根据key快速查找
不同点
- react在diff遍历的时候,只对需要修改的节点进行了记录,形成effect list,最后才会根据effect list 进行真实dom的修改,修改时先删除,然后更新与移动,最后插入
- vue 在遍历的时候就用真实dom
insertBefore
方法,修改了真实dom,最后做的删除操作
- react 采用单指针从左向右进行遍历
- vue采用双指针,从两头向中间进行遍历
- react的虚拟diff比较简单,vue中做了一些优化处理,相对复杂,但效率更高