VUE DIFF算法系列讲解
前言
最近的几次面试中,发现不少中高级前端工程师,都会写到了解vue底层设计原理,那既然人写都写了,还不得问一下子,但是看大家的回答效果,建议还是不要写原理相关内容了。
在最近的几场面试中,大家对这个问题的回答其实都是答的双端diff算法,其实如果你真的不太熟悉双端diff,面试官又问了,那还不如给出一个自己的diff算法,起码做到先能用,再优化,别尬在现场。本文中提到的算法,个人感觉大部分人应该还是比较好理解的,也能较快的转化成自己的内容。下面我们就通过代码实现及实际案例,详细了解一下
一、简单DIFF算法的代码实现
首先说一下diff主要是干什么?diff其实就是通过新旧vdom的对比,通过移动,新增和删除,在保证olddom能被最大复用的情况下,将旧的虚拟dom,变成新的虚拟dom的样子。
下边我们展示下实现的代码,代码都有详细的注释,大家如果看到觉得有问题或者不太理解的地方,希望可以帮忙反馈在评论区。
提示:不需要特别关注patch等函数的实现,和diff的关系不是很大。理解其大致想做的事即可,具体模拟实现可参考文末github链接
// n1 旧的vdom n2 新的vdom组
const oldChildren = n1.children;
const newChildren = n2.children;
const oldLen = oldChildren.length;
const newLen = newChildren.length;
// 加key的情况下
// 用来存储在寻找过程中,遇到的最大索引值(oldChildren中的索引)
let lastIndex = 0;
for (let i = 0; i < newLen; i++) {
const newVnode = newChildren[i];
// 定义一个变量。来存储当前的newVnode是否在旧的节点中被找到
let find = false;
for (let j = 0; j < oldLen; j++) {
const oldVnode = oldChildren[j];
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode, container);
if (j < lastIndex) {
// 如果当前找到的节点在oldchildren中的索引值小于最大索引值 lastIndex,
// 说明该节点需要进行移动
const preVnode = newChildren[i - 1];
// 如果preVnode不存在,说明他是第一个位置,不需要移动
if (preVnode) {
// 由于我们要将newVnode对应的真实DOM节点移动到preVnode对应的真实DOM后
// 所以我们获取newVnode对应的真实DOM节点的下一个节点作为锚点
const anchor = preVnode.el.nextSibling;
insert(newVnode.el, container, anchor);
}
} else {
lastIndex = j;
}
break;
}
}
// 如果当前newVnode没有被找到,也就说明此节点为新增节点
if (!find) {
const preVnode = newChildren[i - 1];
let anchor = null;
if (preVnode) {
// 如果当前节点有一个node节点,则使用它的下一个兄弟节点作为锚点
anchor = preVnode.el.nextSibling;
} else {
// 如果没有,则使用container的第一个节点作为锚点
anchor = container.firstChild;
}
// 如果anchor没找到,则 newNode 将被插入到子节点的末尾
patch(null, newVnode, container, anchor);
}
}
// 完成上一步操作后,遍历一下旧的节点,看哪些需要卸载
for (let i = 0; i < oldChildren.length; i++) {
// 拿旧的节点在新的节点中找
const has = newChildren.find(vnode => vnode.key === oldChildren[i]);
if (!has) {
unmount(oldChildren[i]);
}
}
二、实践
1. 练习1
// oldChildrem
p-1 p-2 p-3
// newChildren
p-3 p-1 p-4 p-2
初始情况下,lastIndex = 0
对newChildren和oldChildrem进行嵌套循环,
i = 0 new old find
j = 0: p-3 p-1 false
j = 1: p-3 p-2 false
j = 2: p-3 p-3 true
此时,j >= lastIndex, 所以将lastIndex更新为2, 位置不需要移动; 找到对应节点后,break掉本次循环。此时节点情况如下:
p-1 p-2 p-3
i = 1 new old find
j = 0: p-1 p-1 true
此时,j < lastIndex, 位置需要移动,锚点为newChildren[i - 1],所以,锚点为p-3,将其插在p-3后边。找到对应节点后,break掉本次循环。此时节点情况如下:
p-2 p-3 p-1
i = 2 new old find
j = 0: p-4 p-1 false
j = 1: p-4 p-2 false
j = 2: p-4 p-3 false
因为本次循环中,new vnode在 oldChildren中没有找到,所以需要对此节点进行挂载。挂载锚点为newChildren[i - 1]即p-1,所以把此节点插在p-1之后,此时节点情况如下:
p-2 p-3 p-1 p-4
i = 3 new old find
j = 0: p-2 p-1 false
j = 1: p-2 p-2 true
此时,j < lastIndex, 位置需要移动,锚点为newChildren[i - 1],所以,锚点为p-4,将其插在p-4后边。找到对应节点后,break掉本次循环。此时节点情况如下:
p-3 p-1 p-4 p-2
循环结束后,需要单独循环oldChildren, 看哪些oldVnode在newChildren中没有,此demo中,oldVnode均可在newChildren找到,所以不做任何操作
2. 练习2
// oldChildrem
p-1 p-2 p-3
// newChildren
p-4 p-2
初始情况下,lastIndex = 0
对newChildren和oldChildrem进行嵌套循环,
i = 0 new old find
j = 0: p-4 p-1 false
j = 1: p-4 p-2 false
j = 2: p-4 p-3 false
因为本次循环中,new vnode在 oldChildren中没有找到,所以需要对此节点进行挂载。挂载锚点为newChildren[i - 1]即undefined,因前边无节点,所以将锚点设置为此container的第一个元素,将其放在其对前边,此时顺序为
p-4 p-1 p-2 p-3
i = 1 new old find
j = 0: p-2 p-1 false
j = 1: p-2 p-2 true
此时,j <= lastIndex, 位置不需要移动,此时顺序为
p-4 p-1 p-2 p-3
循环结束后,需要单独循环oldChildren, 看哪些oldVnode在newChildren中没有,此demo中,p-1和p-3均无法在newChildren中找到,将其卸载此时顺序为
p-4 p-2
总结
本文主要讲解了简单diff的代码实现,并通过两个例子进行了深入的理解,因为此算法比较简单,所以应该更容易转化成为我们自己的东西。在面试过程中,如果真的记不起双端算法,也可以回答下简单diff算法。
参考:<<vue设计与实现>>第9章
github:link