这是 聊diff 的第二篇文章,聊聊vue2的diff思路.思路主要来自 vue-design 项目
【第一篇】和面试官聊聊Diff___React
【第二篇】和面试官聊聊Diff___vue2(本文)
【第三篇】和面试官聊聊Diff___Vue3
为了更好的阅读体验,建议从第一篇看起
我是一名前端的小学生。行文中对某些设计原理理解有误十分欢迎大家讨论指正😁😁😁,谢谢啦!当然有好的建议也谢谢提出来
(玩笑)
Let's start
vue2_diff
比对流程
本文注重的是patch过程,具体的细节和边界就没有考虑。
==另 外 注 意==
- 三篇文章 diff 的讲解,为了方便展示 节点复用, 用了
children
保存内容,实际上这是不合理的,因为children不同还会递归补丁(patch)- diff也不是vue optimize的全部,只是其中一部分,例如compile时确定节点类型,不同类型 不同的
mount/patch
处理方式等等。
Vue2.x的 diff 相对于 react 更好一些,避免了一些不必要的比对。
基本思路
现在比如说由若干个新老节点(preNodes
/ nextNodes
)。
使用nextStartIdx
、nextEndIdx
、nextStartNode
、nextEndNode
保存 ==新节点== 首尾索引和首尾节点。
使用preStartIdx
、preEndIdx
、preStartNode
、preEndNode
保存 ==老节点== 首尾索引和首尾节点。
先将新旧节点首尾一一比对,遇到
undefined
跳过- 如果相同则
前移
/后移
比对节点(首首相同,均后移;尾尾相同,均前移;首尾相同,新首后移,旧尾前移;尾首相同)。 - 如果存在 首尾遍历了全部节点, 那么 删除多余节点(情况①)或新增节点(情况②)即可
- 如果相同则
相同的都比对完毕后,将新节点(
nextNodes
)中未比对的每个节点与老节点(preNodes)中未比对节点(是一个key
和index
对应的map
)中所有节点比对- 如果存在相同,将老节点中相应节点重置为
undefined
, 否者在 新生成节点中 添加/删除相应节点。
- 如果存在相同,将老节点中相应节点重置为
情况①(也可能是首尾/尾首比对),再继续循环, 会有 preStartIdx > preEndIdx
, 表明有新增节点,要添加
情况②(也可能是首尾/尾首比对),再继续循环, 会有 nextStartIdx > nextEndIdx
, 表明有多余节点,要删除
代码体现:
if(preStartIdx > preEndIdx) { // 有连续多余的新节点需增加
const addStartIndex = nextStartIdx > 0 ? nextStartIdx-1 : 0;
let insertIndex = newNodes.findIndex(node => nextNodes[addStartIndex].key === node.key)
for(let i = nextStartIdx; i<= nextEndIdx; i++){
newNodes.splice(insertIndex +1, 0, nextNodes[i])
insertIndex++;
}
}else if(nextStartIdx > nextEndIdx) {
//有连续多余的旧节点,需删除(末尾开始删除) 123456 | 1256
// const delStartIndex = preStartIdx > 0 ? preEndIdx -1 : 0;
let deleteIndex = newNodes.findIndex(node => preNodes[preEndIdx].key === node.key)
for(let i=preEndIdx; i>=preStartIdx; i--){
if(preNodes[i] === undefined) continue;
newNodes.splice(deleteIndex, 1);
deleteIndex--;
}
}
比如有以下节点:
// 旧节点
const preNodes = [
{key: "k-1", children: "<span>old1</span>"},
{key: "k-2", children: "<span>old2</span>"},
{key: "k-3", children: "<span>old3</span>"},
{key: "k-4", children: "<span>old4</span>"},
{key: "k-5", children: "<span>old5</span>"},
{key: "k-6", children: "<span>old6</span>"},
{key: "k-7", children: "<span>old7</span>"},
{key: "k-8", children: "<span>old8</span>"},
]
// 新节点
const nextNodes = [
{key: "k-8", children: "<span>8</span>"},
{key: "k-7", children: "<span>7</span>"},
{key: "k-12", children: "<span>12</span>"},
{key: "k-5", children: "<span>5</span>"},
{key: "k-3", children: "<span>3</span>"},
{key: "k-11", children: "<span>11</span>"},
{key: "k-2", children: "<span>2</span>"},
{key: "k-13", children: "<span>13</span>"},
{key: "k-9", children: "<span>9</span>"},
{key: "k-1", children: "<span>1</span>"},
]
经过补丁diff后,期望得到结果
可以看到老节点都得到了复用~~
用图来具体实现上面节点变换过程。
图例讲解
==注意==: 图中删除插入是我用数组代替节点插入 的操作( 例如insertbefore
),其实正常 节点插入 时,就会检测插入节点是否存在,如果存在是不会插入重复节点的,只会将原来节点调换至新的位置。
例子1
这个例子就是把上面提到的图形解剖一步步呈现。一大波图来袭
### 说明
- 使用
nextStartIdx
、nextEndIdx
、nextStartNode
、nextEndNode
保存 ==新节点== 首尾索引和首尾节点。- 使用
preStartIdx
、preEndIdx
、preStartNode
、preEndNode
保存 ==老节点== 首尾索引和首尾节点。- 绿色表示新增节点,红色表示删除节点,湖蓝色表示复用节点。
初始状态
preStartIdx = 0;preEndIdx = 7;nextStartIdx = 0;nextEndIdx = 9;
尾首相同,将旧首节点(k-1
)插入到旧尾节点(k-8
)后面。且 preStartIdx + 1, nextEndIdx -1
preStartIdx = 1;preEndIdx = 7;nextStartIdx = 0;nextEndIdx = 8;
尾首相同,将旧尾节点( k-8
)插入到旧首节点(k-2
)前面。且 nextStartIdx + 1, preEndIdx -1
preStartIdx = 1;preEndIdx = 6;nextStartIdx = 1;nextEndIdx = 8;
尾首相同,将旧尾节点(k-7
)插入到旧首节点(k-2
)前面。且 nextStartIdx + 1, preEndIdx -1
preStartIdx = 1;preEndIdx = 5;nextStartIdx =2;nextEndIdx = 8;
两边(首尾)比对完成,开始中间比对(newNodes
未比对节点与 preNodes
未比对所有节点一一比对),k-12
在老节点未比对区间无相同,增加到k-2前面
preStartIdx = 1;preEndIdx = 5;nextStartIdx = 3;nextEndIdx = 8;
找到 k-5
相同(复用),k-5
增加到 k-2
前面,删除原来( newNodes
)的 k-5
,老节点中 k-5
置为undefined
preStartIdx = 1;preEndIdx = 5;nextStartIdx = 4;nextEndIdx = 8;
找到 k-3
相同(复用),k-3
增加到 k-2
前面,删除原来( newNodes
)的 k-3
,老节点中 k-3
置为undefined
preStartIdx = 1;preEndIdx = 5;nextStartIdx = 5;nextEndIdx = 8;k-11
未找到相同节点,k-11
增加到 k-2
前面
preStartIdx = 1;preEndIdx = 5;nextStartIdx = 6;nextEndIdx = 8;
首首节点 k-2
相同,k-2
增加到 k-2
前面, 删除原来的 k-2
因为undefined会跳过,即preStartIdx = 3;preEndIdx = 5;nextStartIdx = 0;nextEndIdx = 9;k-13
未找到相同节点,k-13
增加到 k-4
前面
preStartIdx = 3;preEndIdx = 5;nextStartIdx = 8;nextEndIdx = 8;
新节点前后索引相等,k-9
添加到 k-4
前面
满足 nextStartIdx >nextEndIdx
删除多余节点
最后结果
兄弟们,点个赞啊,累死我了,哎~~~(图搞了我两个多小时)
加深理解的例子
这个例子节点为;
const preNodes = [
{key: "k-1", children: "<span>old1</span>"},
{key: "k-2", children: "<span>old2</span>"},
{key: "k-3", children: "<span>old3</span>"},
{key: "k-4", children: "<span>old4</span>"},
{key: "k-5", children: "<span>old5</span>"},
{key: "k-6", children: "<span>old6</span>"},
{key: "k-7", children: "<span>old7</span>"},
{key: "k-8", children: "<span>old8</span>"},
{key: "k-9", children: "<span>old9</span>"},
]
// /* 旧首==新尾 旧尾==新首 旧尾==新首 首首 旧首==新尾 尾尾*/
const nextNodes = [
{key: "k-9", children: "<span>9</span>"},
{key: "k-12", children: "<span>12</span>"},
{key: "k-8", children: "<span>8</span>"},
{key: "k-13", children: "<span>13</span>"},
{key: "k-6", children: "<span>6</span>"},
{key: "k-2", children: "<span>2</span>"},
{key: "k-15", children: "<span>15</span>"},
{key: "k-5", children: "<span>5</span>"},
{key: "k-14", children: "<span>14</span>"},
{key: "k-19", children: "<span>19</span>"},
{key: "k-7", children: "<span>7</span>"},
{key: "k-3", children: "<span>3</span>"},
{key: "k-1", children: "<span>1</span>"},
]
为什么特殊呢,因为会发生首尾和中间比对交叉进行(其实本来就相互影响),
这个我就不去画图解释了,这个例子为加深理解所用。
代码实现
代码
直接贴代码了
function vue2_diff(){
const preNodes = [
{key: "k-1", children: "<span>old1</span>"},
{key: "k-2", children: "<span>old2</span>"},
{key: "k-3", children: "<span>old3</span>"},
{key: "k-4", children: "<span>old4</span>"},
{key: "k-5", children: "<span>old5</span>"},
{key: "k-6", children: "<span>old6</span>"},
{key: "k-7", children: "<span>old7</span>"},
{key: "k-8", children: "<span>old8</span>"},
]
const nextNodes = [
{key: "k-8", children: "<span>8</span>"},
{key: "k-7", children: "<span>7</span>"},
{key: "k-12", children: "<span>12</span>"},
{key: "k-5", children: "<span>5</span>"},
{key: "k-3", children: "<span>3</span>"},
{key: "k-11", children: "<span>11</span>"},
{key: "k-2", children: "<span>2</span>"},
{key: "k-13", children: "<span>13</span>"},
{key: "k-9", children: "<span>9</span>"},
{key: "k-1", children: "<span>1</span>"},
]
let preStartIdx = 0;
let nextStartIdx = 0;
let preStartNode = preNodes[0];
let nextStartNode = nextNodes[0];
let preEndIdx = preNodes.length - 1;
let nextEndIdx = nextNodes.length - 1;
let preEndNode = preNodes[preEndIdx];
let nextEndNode = nextNodes[nextEndIdx];
const newNodes = JSON.parse(JSON.stringify(preNodes));
function isSameNode(node1, node2){
return node1.key === node2.key;
}
while(preStartIdx <= preEndIdx && nextStartIdx <= nextEndIdx){
if(preStartNode === undefined){
console.log('undefined: ', preStartNode);
preStartNode = preNodes[++preStartIdx];
}else if(preEndNode === undefined){
console.log('undefined: ', preEndNode);
preEndNode = preNodes[--preEndIdx]
}else if(isSameNode(preStartNode, nextStartNode)){
// 新旧首首相同
preStartNode = preNodes[++preStartIdx];
nextStartNode = nextNodes[++nextStartIdx];
}else if(isSameNode(preEndNode, nextEndNode)) {
// 新旧尾尾相同
preEndNode = preNodes[--preEndIdx];
nextEndNode = nextNodes[--nextEndIdx];
}else if(isSameNode(preStartNode, nextEndNode)) {
//执行 将旧首节点插入到旧尾节点后面
const insertPos = newNodes.findIndex(node => node.key === preEndNode.key);
const deletePos = newNodes.findIndex(node => node.key === preStartNode.key);
// 添加
newNodes.splice(insertPos+1, 0, preStartNode);
// 删除
const delIndex = insertPos < deletePos ? deletePos + 1 : deletePos;
newNodes.splice(delIndex, 1);
preStartNode = preNodes[++preStartIdx];
nextEndNode = nextNodes[--nextEndIdx];
}else if(isSameNode(preEndNode, nextStartNode)) {
//执行 将旧尾节点插入(insertBefore)到旧首节点前面
const insertPos = newNodes.findIndex(node => node.key === preStartNode.key);
const deletePos = newNodes.findIndex(node => node.key === preEndNode.key);
// 添加
newNodes.splice(insertPos, 0, preEndNode);
// 删除
const delIndex = insertPos < deletePos ? deletePos + 1 : deletePos;
newNodes.splice(delIndex, 1);
nextStartNode = nextNodes[++nextStartIdx];
preEndNode = preNodes[--preEndIdx];
}else {
const oldKeyInIndex = {};
for(let i=preStartIdx; i<=preEndIdx; i++){
if(!preNodes[i]) continue; // 处理undefined情况
oldKeyInIndex[preNodes[i].key] = i;
}
const moveNodeIndex = oldKeyInIndex[nextStartNode.key]
if(moveNodeIndex !== undefined){
//找到即 insertBefore,将当前节点插入到旧开始节点在newNodes的位置的后面
const insertPos = newNodes.findIndex(node => node.key === preStartNode.key);
const deletePos = newNodes.findIndex(node => node.key === preNodes[moveNodeIndex].key);
newNodes.splice(insertPos, 0, preNodes[moveNodeIndex]);
newNodes.splice(deletePos+1, 1);
preNodes[moveNodeIndex] = undefined;
}else {//添加
const insertPos = newNodes.findIndex(node => preStartNode.key === node.key);
newNodes.splice(insertPos, 0, nextStartNode);
}
nextStartNode = nextNodes[++nextStartIdx];
}
}
if(preStartIdx > preEndIdx) { // 有连续多余的新节点需增加
const addStartIndex = nextStartIdx > 0 ? nextStartIdx-1 : 0;
let insertIndex = newNodes.findIndex(node => nextNodes[addStartIndex].key === node.key)
for(let i = nextStartIdx; i<= nextEndIdx; i++){
newNodes.splice(insertIndex +1, 0, nextNodes[i])
insertIndex++;
}
}else if(nextStartIdx > nextEndIdx) {
//有连续多余的旧节点,需删除(末尾开始删除) 123456 | 1256
// const delStartIndex = preStartIdx > 0 ? preEndIdx -1 : 0;
let deleteIndex = newNodes.findIndex(node => preNodes[preEndIdx].key === node.key)
for(let i=preEndIdx; i>=preStartIdx; i--){
if(preNodes[i] === undefined) continue;
newNodes.splice(deleteIndex, 1);
deleteIndex--;
}
}
console.log('vue2 diff: ', newNodes);
}
总结
本文中例子只是为了更好理解diff思路, patch过程与真实情况还有些差异
- 重复节点问题。新老节点有重复节点时,本文diff函数没处理这种情况。
- 仅是用数组模拟了Vnode,真实的Vnode 不止 key和children,还有更多的参数
- 比对相同节点时,仅比对了 key, 真实其实还涉及到 class(类名) 、attrs(属性值)、孩子节点(递归)等属性比对;另外上面的children也要比对若不同也要递归遍历
- 插入、删除、添加节点我用的数组。其实应该用
insertbefore
、delete
、add
。这些方法均是单独封装不能采用相对应的 Dom Api,因为 vue 不止用在浏览器环境。 - ...
Vue@3.2
⇲ 已经出来了,React@18
也快了,哎,框架学不完的。还是多看看不变的东西吧(js, 设计模式, 数据结构,算法...)
哎哎哎,,同志,看完怎么不点赞,别看别人就说你呢,你几个意思?
参考
站在别人肩膀能看的更远。
【推荐】vue-design
【掘金小册】剖析Vue.js内部运行机制
【Vue patch源码地址】vue-next ⇲
以上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。