1.前言
Vue和React中都有Vnode(虚拟节点)
,它的作用是便于更新Dom节点,如果没有Vnode
的话每更新一次数据都会做一次dom操作,极其的影响执行效率浪费资源。而用了Vnode之后,每次数据更新之后会首先在Vnode虚拟节点上进行更新,然后在通过VnodeList对dom进行操作,将dom更新集中在一起进行,也避免的很多冗余的操作,比如一个节点先更新了内容再进行了卸载,用了Vnode后就之后执行卸载这一步。
2.什么是Diff算法? 第一节我们说了Vue中会通过Vnode更新dom节点,更新的过程包括:`判断节点是否可以复用、如何复用节点、如何卸载节点、如何新增节点`。而Diff算法就是用来优化这个过程的,不同的优化过程就是不同的算法,和它的名字一样`diff(different)`找到不同并进行修改。 在了解Diff算法前,我们得来了解一下一些必须的前置变量: - `newVnodeList:`新的虚拟节点列表 - `oldVnodeList:`旧的虚拟节点列表 - `newStart/oldStart:`一个变量,值为newVnodeList/oldVnodeList当前开始的index - `newEnd/oldEnd:` 一个变量,值为newVnodeList/oldVnodeList当前结尾的index
Diff的基本步骤:
- 找到可以复用的节点(一般都是通过比较newVnodeList、oldVoneList中各项的key值,若key值相同则视为可复用)
- 更新(patch)并移动可以复用的节点
- 卸载(unmount)多余的节点(一般是当newVnodeList为空时,oldVnodeList多余的节点进行卸载)
- 新增(insert)全新的节点(一般是newVnodeList的一个节点item,item在oldVnodeList中都找不到key值相同的节点时,则视为该节点全新节点)
Tip: 如何更新、卸载、新增、移动节点这部分知识是渲染器那部分的,和Diff算法关系不大。后面会有空更新渲染器的渲染原理。
3.Vue中的Diff算法有哪些?
Vue中共有3中Diff算法,简单Diff算法、双端Diff算法、快速Diff算法,其中快速Diff算法的性能最优,也是现在Vue3使用的Diff算法。双端Diff算法是Vue2中使用的,总的来说 快速Diff算法优于双端Diff算法,双端Diff算法优于简单Diff算法。
简单 Diff算法
找到可复用的节点
如何判断一个节点是否可以复用,简单来说就是从newVnodeList中取出一个虚拟节点V1,并遍历oldVnodeList去找和V1的key值相同的节点,若找到了则标记V1可复用,若找不到,则标记V1不可复用,进行新增操作。
在简单Diff算法中,通过使用2个for循环,来判断哪些节点可以复用
js
for(let i = 0; i < newVnodeList.length; i++) {
for(let j = 0; j < oldVnodeList.length; j++) {
if(newVnodeList[i].key == oldVnodeList[i].key){
// 可复用
patch(oldVnodeList[i], newVnodeList[i], container); // 执行更新操作,为后面的复用做准备
break;
}
}
}
移动可复用的节点
拿newVnodeList中的每一项item去oldVnodeList中进行对比,找到了相同key的节点,就记录一下当前item在oldVnodeList中的index,并与maxIndex进行比较,若大于maxIndex则不需要移动,若小于则需要移动。 很好理解,就是比较2个节点的先后顺序,如果在新旧虚拟节点List中先后顺序都相同的话,就不需要移动,如果不同的话则一定需要移动。
js
let maxIndex = -1;
for(let i = 0; i < newVnodeList.length; i++) {
for(let j = 0; j < oldVnodeList.length; j++) {
if(newVnodeList[i].key == oldVnodeList[i].key){
// 可复用
patch(oldVnodeList[i], newVnodeList[i], container);
if(j > maxIndex) {
// 不移动节点
maxIndex = j;
} else {
// 移动节点
}
break;
}
}
}
卸载多余的节点
拿oldVnodeList中的每一项item去newVnodeList中寻找,找不到就说明需要卸载。
js
for(let i = 0; i < oldVnodeList.length; i++) {
if(!newVnodeList.find((item) => {
return item.key == oldVnodeList[i].key
})) {
unmount(oldVnodeList[i])
}
}
新增全新的节点
拿newVnodeList中的每一项item去oldVnodeList中寻找,找不到就说明需要新增。
js
for(let i = 0; i < newVnodeList.length; i++) {
let find = false;
for(let j = 0; j < oldVnodeList.length; j++) {
if(newVnodeList[i].key == oldVnodeList[i].key){
// 可复用
patch(oldVnodeList[i], newVnodeList[i], container); // 执行更新操作,为后面的复用做准备
...
find = true;
break;
}
}
if(!find) {
// 执行新增操作
}
}
执行完这四项,dom节点也被更新完毕了。
双端 Diff算法
js
while(newStart < newEnd && oldStart < oldEnd) {
if(nd[newStart].key == od[oldStart].key) {
// 更新操作
newStart++;
oldStart++;
} else if (nd[newEnd].key == od[oldEnd].key) {
// 更新操作
newEnd--;
oldEnd--
} else if (nd[newEnd].key == od[oldStart].key) {
// 更新操作
newEnd--;
oldStart++;
} else if (nd[newStart].key == od[oldEnd].key) {
// 更新操作
newStart++;
oldEnd--;
} else {
// 双端都没有,就去内部找
let find = flase;
for(let i = oldStart + 1; i <= oldEnd; i++) {
if(nd[newStart].key == od[i].key) { // 找到了对应节点
newStart--;
patch // 更新操作
insert // 进行移动
find = true;
od[i] = undefined; // 已经被移动了,此处设undefined
}
}
if(!find) {
patch // 新增节点,执行新增操作
}
}
}
if(oldEnd >= oldStart) {
for(let i = oldStart; i <= oldEnd; i++) {
unmount(od[i]) // 卸载对应的节点
}
}
快速 Diff算法
js
// 预处理
while(nStart < nEnd && oStart < oEnd) {
if(nd[nStart].key == od[oStart].key) {
patch // 更新
nStart++;
oStart++;
} else if(nd[nEnd].key == od[oEnd].key) {
patch // 更新
nEnd--;
oEnd--;
} else {
break;
}
}
// 开始Diff算法
let keyObj = {}
nd.forEach(item,index => {
keyObj[item.key] = index;
})
let indexArr = new Array(nStart - nEnd + 1).fill(-1)
od.forEach(item,index => {
if(keyObj[item.key] == undefined)
unmount(item) // 执行卸载操作
indexArr[keyObj[item.key] - nStart] = index;
})
let seq = findAdd(indexArr) // 找到最大递增子序列
let s = seq.length - 1;
let i = indexArr.length - 1;
for(let j = i; j > 0; j--) {
if(indexArr[j] == '-1') {
// 新增节点
} else if(indexArr[j] == seq[s]){
// 不移动;
patch;
s--;
] else {
// 该节点需要移动
ptach;
insert;
}
}
4.总结
功力不够,无法用自己的语言简单的表达处理,只能通过代码的形式输出出来,省略了很多优化的步骤,只将最核心的功能写了出来。