关注公众号"知之洲"可以阅读本文全篇和其他优质干货。
原因
如果不进行对比,每次更新的时候都先卸载旧节点,然后挂载新节点,每次卸载或挂载都会造成页面重排,浪费性能,因此需要进行一些细粒度的操作,最好找到所有不同点,根据这些不同点进行细粒度的按需同步。可以从以下三个地方入手:
- 找到对相同的节点。(复用)
- 找出需要新增的虚拟DOM。(创建并插入)
- 元素发生位置变化,找出哪些元素需要移动。(移动)
调用时机
修改响应式属性会重新执行render函数,返回新虚拟DOM。这时候会调用patch对比新旧虚拟DOM,并返回一个patch对象,用来存储两个节点不同的地方,最后根据patch对象去更新DOM。
调用patch对比新旧虚拟DOM的时候,如果双方子级都为数组的情况下(具有多个子级节点)就会调用patchChildren方法,patchChildren方法里面会根据有没有key分成个大的方向去比较双方子级元素的差异,这里使用到的算法就是diff算法,只会进行同层比较不会进行跨层比较。
function patchChildren(...) {
if (/*存在key*/) {
patchKeyedChildren(oldChildren, newChildren, el);
} else {
patchUnkeyedChildren(oldChildren, newChildren, el);
}
}
无key的diff算法
此种情况很简单,直接轮询。从头开始轮询,根据情况执行patch、unmount、mount操作。
function patchUnkeyedChildren(oldChildren, newChildren, el) {
const oldLen = oldChildren.length;
const newLen = newChildren.length;
const commonLen = Math.min(oldLen, newLen);
for (let i = 0; i < commonLen; i++) {
patch(/*对比新旧节点*/);
}
if (oldLen > newLen) {
unmount(/*卸载后面的节点*/);
} else {
mount(/*挂载后面的节点*/);
}
}
公共对比中a、b可以复用,对比到旧节点为c后,新虚拟DOM变为f了,patch后f会替换掉c,之后的公共对比会一直替换下去。
替换过程:判断节点类型,如果相同就会复用旧的真实节点,如果不同就会删除旧的真实节点,创建新的真实节点再进行挂载。类型相同虽然会复用旧的真实节点,但仍然需要比较两个虚拟DOM,因为它们的属性、内容可能不同,然后还要对比它们的子级,如此递归。
公共对比完成之后,后面的要么是直接删除旧的真实节点,完成卸载。要么是是直接插入新的真实节点完成挂载。
有key的diff算法
从前往后递增对比,节点类型和key相同的进行复用(在不同处停止对比)。
while (i <= e1 && i <= e2) {
const oldNode = oldChildren[i];
const newNode = newChildren[i];
if (isSameVNodeType(oldNode, newNode)) {
patch(/*对比新旧节点*/);
} else {
break
}
i++
}
从后往前递减对比,节点类型和key相同的进行复用(在不同处停止对比)。
while (i <= e1 && i <= e2) {
const oldNode = oldChildren[i];
const newNode = newChildren[i];
if (isSameVNodeType(oldNode, newNode)) {
patch(/*对比新旧节点*/);
} else {
break
}
e1--;
e2--;
}
此时,如果 i > e1 说明旧节点列表经过首尾两次对比已经被全部覆盖到,这时如果 i < e2 说明有新增节点。
如果 i > e2 说明新节点列表经过首尾两次对比已经被全部覆盖到,这时如果 i < e1 说明有需要删除的节点。
如果 i < e1 并且 i < e2,说明经过首尾两次对比新旧节点都未被全部覆盖到,此时说明中间存在乱序节点。
对于乱序的情况,新增s1和s2指针,都指向在i停止的位置。
- 把新子级的所有乱序元素(e、c)保存在一个映射表里(keyToNewIndexMap)。
- 遍历旧乱序子级,查找映射表中是否存在该元素(判断key和类型是否一致),存在就进行复用,不存在就说明需要创建新元素并插入,还要把旧节点进行删除。
关注公众号“知之洲”还可以阅读更多优质干货。