「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
为什么要分析diff算法?
- 了解清楚diff算法,有助于写出对页面dom更新的性能更高的代码。
- 锻炼自己的解决问题的思维,在工作中遇到一些问题,能给出更好的解决方案。
diff算法原则?
- 能不移动,尽量不移动
- 没得办法,只好移动
- 实在不行,新建或删除
1、patch来自哪?
var patch = createPatchFunction();
Vue.prototype.__patch__ = patch
2、patch来自createPatchFunction,然后我们分析这个函数
function createPatchFunction() {
return function patch(oldVnode, vnode, parentElm, refElm) {
// 页面首次加载-->没有旧节点,直接生成新节点
if (!oldVnode) {
createElm(vnode, parentElm, refElm);
}
else {
// 对比新旧节点,先简单的认为只是对比根节点的tag和key就好
// 实际上还对比了isComment、asyncFactory等;
// 相同时
if (sameVnode(oldVnode, vnode)) {
// 比较存在的根节点(**重点分析**)
patchVnode(oldVnode, vnode);
}
// 不同时
// 生成新节点,同时删除旧节点
else {
// 替换存在的元素
var oldElm = oldVnode.elm;
var _parentElm = oldElm.parentNode
// 创建新节点
createElm(vnode, _parentElm, oldElm.nextSibling);
// 销毁旧节点
if (_parentElm) {
removeVnodes([oldVnode], 0, 0);
}
}
}
return vnode.elm
}
}
3、分析patchVnode函数(新旧node的根节点tag和key相同时)
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) return
var elm = vnode.elm = oldVnode.elm;
var oldCh = oldVnode.children;
var ch = vnode.children;
// vnode不是文本节点
if (!vnode.text) {
// vnode和oldVnode都有子节点且不相同
if (oldCh && ch) {
if (oldCh !== ch) {
// (**重点分析**)
updateChildren(elm, oldCh, ch);
}
}
// 只有vnode有子节点,oldVnode,没有
else if (ch) {
// 如果旧节点是本文本节点,删除其文本内容
if (oldVnode.text) elm.textContent = '';
// 遍历创建vnode的子节点
for (var i = 0; i <= ch.length - 1; ++i) {
createElm(
ch[i],elm, null
);
}
}
// 只有oldVnode有子节点,vnode,没有
else if (oldCh) {
// 遍历删除oldVnode的子节点
for (var i = 0; i<= oldCh.length - 1; ++i) {
oldCh[i].parentNode.removeChild(el);
}
}
// 两个都没有子节点,并且oldVnode是文本节点
// 删除其中的文本内容
else if (oldVnode.text) {
elm.textContent = '';
}
}
// vnode是纯文本节点,且与oldVnode文本不同
// 替换oldVnode中的内容
else if (oldVnode.text !== vnode.text) {
elm.textContent = vnode.text;
}
}
4、updateChildren(新旧node都有子节点且不同时)
这部分比较复杂,先理清思路再看代码:
(1)定义8个变量分别为:
var oldStartIdx = 0; // olcVnode起始的index
var oldEndIdx = oldCh.length - 1; // olcVnode结束的index
var oldStartVnode = oldCh[0]; // olcVnode起始的节点
var oldEndVnode = oldCh[oldEndIdx]; // olcVnode结束的节点
var newStartIdx = 0; // vnode起始的index
var newEndIdx = newCh.length - 1; // vnode结束的index
var newStartVnode = newCh[0]; // vnode起始的节点
var newEndVnode = newCh[newEndIdx // vnode结束的节点
(2)对比
方式:旧头 == 新头、旧尾 == 新尾、旧头 == 新尾、旧尾 == 新头、逐个对比
概述过程:
1、利用while循环做对比,如果前4种对比有对比成功,则执行相应操作并对应的去更新oldStartIdx 、oldEndIdx 、oldStartVnode 、oldEndVnode ,然后进行下一轮对比。
2、如果四个都没通过,则通过修改newStartIdx来遍历vnde与oldVnode进行单个对比,处理oldVnode。最后可能oldVnode被处理完,也可能vnode被遍历完。都会停止while,最后处理剩下的那一组的节点。
详细过程:
(1)旧头 == 新头
true=>执行下面操作,然后重新进入while循环
patchVnode(oldStartVnode, newStartVnode); // 递归子节点
oldStartVnode = oldCh[++oldStartIdx]; // oldStartIdx加1
newStartVnode = newCh[++newStartIdx]; // newStartIdx加1
false=>下一步
(2)旧尾 == 新尾
true=>执行下面操作,然后重新进入while循环
patchVnode(oldEndVnode, newEndVnode); // 递归子节点
oldEndVnode = oldCh[--oldEndIdx]; // oldStartIdx减1
newEndVnode = newCh[--newEndIdx]; // newEndIdx减1
false=>下一步
(3)旧头 == 新尾
true=>执行下面操作,然后重新进入while循环
patchVnode(oldStartVnode, newEndVnode); // 递归子节点
// oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
oldStartVnode = oldCh[++oldStartIdx]; // oldStartIdx加1
newEndVnode = newCh[--newEndIdx]; // newEndIdx减1
false=>下一步
(4)旧尾 == 新头
true=>执行下面操作,然后重新进入while循环
patchVnode(oldEndVnode, newStartVnode); // 递归子节点
// oldEndVnode 放到 oldStartVnode 前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx]; // oldEndIdx减1
newStartVnode = newCh[++newStartIdx]; // newStartIdx减1
false=>逐个对比
(5)逐个对比
1. 生成map表,把就节点的key全部拷贝生成如下
// key值: 对应的vnode节点的索引index
{
1: 0,
2: 1,
4: 2
}
2. 通过map中的key判断vnode的子节点是否存在于oldVnode的子节点数组中
不存在=>创建dom,放在当前oldStartVnode 前面
存在且与vnode子节点相同=> 移动旧dom,放在当前oldStartVnode 前面
存在且与vnode子节点不同=> 创建dom,放在当前oldStartVnode 前面
while结束,处理可能剩下的节点
- 新子节点遍历完了newStartIdx > newEndIdx
删除oldVnode中所有身下的节点
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx]
.parentNode
.removeChild(el);
}
- 旧子节点遍历完了oldStartIdx > oldEndIdx
创建所有剩余的vnode中的子节点。
因为旧节点已经被处理完了。所以我们找到newEndIdx 位置。就能确实节点的倒数有几个已经被处理,
然后把剩下的vnode节点全部放在,倒数被处理过去的dom前
为什么不直接逐个对比,而是要先头尾分别对比四次?
为了处理极端情况,有些刚好是收尾变化,其他没变。如果直接逐个对比,回多走循环,性能变差
function updateChildren(parentElm, oldCh, newCh) {
var oldStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newStartIdx = 0;
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) {
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
}
else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
}
// 旧头 和新头 比较
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
// 旧尾 和新尾 比较
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧头 和 新尾 比较
else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
// oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧尾 和新头 比较
else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// oldEndVnode 放到 oldStartVnode 前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
// 单个新子节点 在 旧子节点数组中 查找位置
else {
// oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
if (!oldKeyToIdx) {
oldKeyToIdx = createKeyToOldIdx(
oldCh, oldStartIdx, oldEndIdx
);
}
// 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
idxInOld = oldKeyToIdx[newStartVnode.key]
// 新孩子中,存在一个新节点,老节点中没有,需要新建
if (!idxInOld) {
// 把 newStartVnode 插入 oldStartVnode 的前面
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
else {
// 找到 oldCh 中 和 newStartVnode 一样的节点
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode);
// 删除这个 index
oldCh[idxInOld] = undefined;
// 把 vnodeToMove 移动到 oldStartVnode 前面
parentElm.insertBefore(
vnodeToMove.elm,
oldStartVnode.elm
);
}
// 只能创建一个新节点插入到 parentElm 的子节点中
else {
// same key but different element. treat as new element
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
}
// 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
newStartVnode = newCh[++newStartIdx];
}
}
// 处理剩下的节点
if (oldStartIdx > oldEndIdx) {
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx], parentElm, refElm
);
}
}
// 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
else if (newStartIdx > newEndIdx) {
for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx].parentNode.removeChild(el);
}
}
}