1.Diff函数的基本思路
简单来说,当新旧vonde的子节点都是一组节点,为了以最小的性能开销来完成更新操作,需要比较两组子节点,用于比较的算法就叫做Dff算法。
首先,以最基本的更新两组子节点的方法为例,通常我们会先将旧节点全部卸载,再安装所有的新节点,那么将会执行旧节点数量+新节点数量次。但有时新节点和旧节点之间,节点类型是想相同的,只有节点内容不同,这是我们可以直接修改节点内容,而省略一次挂载操作。这样就直接将执行时间缩短为之前的一半了。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
if (Array.isArray(n1.children)) {
const oldChildren = n1.children;
const newChildren = n2.children;
for (let i = 0; i < oldChildren.length; i++) {
patch(oldChildren[i], newChildren[i]); // patch函数会比较两个节点之间的差异
}
} else {
// ...
}
}
当然,上述代码的问题也很明显————只有新旧DOM的chidren一样多的时候,以上代码才能正常工作。当新一组节点的长度大于旧节点时吗,将会有新节点被直接挂载,而当旧一组节点的长度大于新节点是,将会有节点被直接卸载。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children;
const newChildren = n2.children;
const oldLen = oldChildren.length; // 旧节点子节点长度
const newLen = newChildren.length; // 新节点子节点长度
const commonLength = Math.max(oldLen, newLen);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 如果旧节点比较长,直接卸载长出来的哪一部分
if (oldLen > newLen) {
for (let i = commonLength; i < oldLen; i++) {
unmount(oldChildren[i]);
}
} else if (newLen > oldLen) {
// 如果新节点比较长,直接挂载长出来哪一部分
for (let i = commonLength; i < newLen; i++) {
patch(null, newChildren[i], container);
}
}
}
}
这样渲染器就能无视数量差异去渲染它们了。
2.DOM复用和key的作用
在上节中,通过减少DOM操作降低了性能消耗,但是节点之间的比较只是最普通的顺序比较,对于存在重复节点的情况并没有考虑,比如说,对于下面两个结点:
const oldNode = {
type: 'div',
children: [
{type: 'p'},
{type: 'span'}
],
}
const newNode = {
type: 'div',
children: [
{type: 'span'},
{type: 'p'}
],
}
如果使用上述代码执行,将会产生四次操作。但实际上,只需要一次交换操作就行了,以为oldNode和newNode之间只是结点的顺序不一样而已。而为了确定新老Node之间是否存在相同结点,就需要引入一个key作为标记,只要两个节点的type和key属性是一样的,那么我们就认为它们是相同节点,但是我们仍然还是要对两个节点进行打补丁的操作,因为节点的内容可能发生了变化。
const oldNode = {
type: 'div',
children: [
{type: 'p', key: 1 },
{type: 'span', key:2 }
],
}
const newNode = {
type: 'div',
children: [
{type: 'span', key: 2 },
{type: 'p', key: 1 }
],
}
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children;
const oldChildren = n1.children;
const newChildren = n2.children;
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container);
break;
}
}
}
}
}
3.标记需要移动的元素
前面我们通过key完成了元素的更新,但是元素的移动还没有实现。这里我们可以用索引的顺序,来判断节点是否需要移动。这里我们使用一个lastIndex变量来存储当前遍历过的最大索引,如果当前遍历的元素在旧children中的索引小于当前最大索引,那么就说明该元素是需要移动的。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children;
const oldChildren = n1.children;
const newChildren = n2.children;
let lastIndex = 0; // 存储点前遍历的最大索引
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container);
if (j < lastIndex) {
// 说明该节点需要移动
} else {
// 否则,更新lastIndex
lastIndex = j;
}
break;
}
}
}
}
}
4.移动元素
标记完需要移动的元素之后,要做的就是移动真实DOM了,这里我们通过vnode获取到它所对应的真实DOM。而当前vnode所在位置,实际上就是在新children中的位置,我们只需要将它插入到当前newChildren节点队列里就行了。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children;
const oldChildren = n1.children;
const newChildren = n2.children;
let lastIndex = 0; // 存储点前遍历的最大索引
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container);
if (j < lastIndex) {
// 说明该节点需要移动
const preNode = newChildren[i - 1];
if (preNode) {
// 获取preNode的下一个兄弟节点,将其作为锚点
const anchor = preNode.el.nextSibling;
// 调用insert方法,将newNode插入到锚点元素前面
insert(newVNode.el, container, anchor);
} else {
// 否则,更新lastIndex
lastIndex = j;
}
break;
}
}
}
}
}
5.添加新元素
对于需要新增的元素,我们同样也需要找到他在新元素列表中的位置。这里我们使用一个find变量,标记是否找到可复用元素,如果没有找到,说明该元素是需要新增的元素。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
// ...
} else if (Array.isArray(n2.children)) {
const oldChildren = n1.children;
const oldChildren = n1.children;
const newChildren = n2.children;
let lastIndex = 0; // 存储点前遍历的最大索引
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
let find = false; // 标记是否找可复用元素
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if (newVNode.key === oldVNode.key) {
find = true; // 一旦找到立即标记
patch(oldVNode, newVNode, container);
if (j < lastIndex) {
// 说明该节点需要移动
const preNode = newChildren[i - 1];
if (preNode) {
// 获取preNode的下一个兄弟节点,将其作为锚点
const anchor = preNode.el.nextSibling;
// 调用insert方法,将newNode插入到锚点元素前面
insert(newVNode.el, container, anchor);
}
} else {
// 否则,更新lastIndex
lastIndex = j;
}
break;
}
}
// 如果没有找到可复用元素,说明当前需要挂载
if (!find) {
const preNode = newChildren[i - 1];
let anchor = null; // 锚点
if (preNode) {
anchor = preNode.el.nextSibling;
} else {
// 如果是第一个元素,获取firstChild作为锚点
anchor = preNode.el.firstChild;
}
patch(null, newVNode, container, anchor);
}
}
}
}
6.移除不存在的元素
同样,对于新children中不存在的元素,我们同样需要进行移除。这里我们对oldChildren进行第二次遍历,查找newChildren是否存在oldChildren中元素的key,如果不存在,说明该元素需要被卸载。
function patchChildren(n1, n2, container) {
if (typeof n2.children === "string") {
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => unmount(child));
}
setElementText(container, n2.children);
} else if (Array.isArray(n2.children)) {
if (Array.isArray(n1.children)) {
const oldChildren = n1.children;
const newChildren = n2.children;
let lastIndex = 0; // 存储点前遍历的最大索引
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
let find = false; // 标记是否找可复用元素
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if (newVNode.key === oldVNode.key) {
find = true; // 一旦找到立即标记
patch(oldVNode, newVNode, container);
if (j < lastIndex) {
// 说明该节点需要移动
const preNode = newChildren[i - 1];
if (preNode) {
// 获取preNode的下一个兄弟节点,将其作为锚点
const anchor = preNode.el.nextSibling;
// 调用insert方法,将newNode插入到锚点元素前面
insert(newVNode.el, container, anchor);
}
} else {
// 否则,更新lastIndex
lastIndex = j;
}
break;
}
}
// 如果没有找到可复用元素,说明当前需要挂载
if (!find) {
const preNode = newChildren[i - 1];
let anchor = null; // 锚点
if (preNode) {
anchor = preNode.el.nextSibling;
} else {
// 如果是第一个元素,获取firstChild作为锚点
anchor = preNode.el.firstChild;
}
patch(null, newVNode, container, anchor);
}
}
// 判断是否有需要移除的元素
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i];
const has = newChildren.find((vnode) => vnode.key === oldVNode.key);
if (!has) {
unmount(oldVNode);
}
}
} else {
setElementText(container, "");
n2.children.forEach((child) => patch(null, child, container));
}
} else {
if (Array.isArray(n1.children)) {
n1.children.forEach((child) => unmount(child));
} else if (typeof n1.child === "string") {
setElementText(container, "");
}
}
}