diff算法
diff算法:
在之前的更新中,每次数据更新,在更新视图时,都是完全产生新的虚拟节点,通过新的虚拟节点生成真实节点,用新生成的真实节点替换所有的老节点。
这种方法在页面元素很少的情况下性能销毁倒是无所谓,但是在页面元素特别多情况下,很明显是消耗很大性能的。哪怕我只是修改了一个dom的文本内容,也都需要重新生成一遍所有节点。(因为现在只有一个组件)
第一次渲染的时候,我们会产生虚拟节点,第二次更新我们也会调用render方法产生新的虚拟节点,我们需要对比两次的vnode,找到需要更新的部分进行更新。
diff的实现,我是建立在前面的代码上的。 实现mini-vue之 computed,watch ,数组响应式的实现
没有key
对于没有key的情况下:vue会在两个vnode的tag相同的时候,就任务是同一个节点。这种情况下可能会出现错误复用。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul>
<li>2</li>
<li>3</li>
<li>1</li>
</ul>
此时vue只会让第一个节点和第一个节点比较,第二个节点和第二个节点比较。
有key
vue在进行diff的时候(新旧虚拟dom都有子节点数组),维护了一个双指针,来进行比较。
let oldStartIndex = 0,
oldEndIndex = oldChildren.length - 1,
newStartIndex = 0,
newEndIndex = newChildren.length - 1,
oldStartVnode = oldChildren[oldStartIndex],
oldEndVnode = oldChildren[oldEndIndex],
newStartVnode = newChildren[newStartIndex],
newEndVnode = newChildren[newEndIndex];
old head -> new head
新旧节点都进行头指针指向的头结点比较。如果两个子节点相同,则会进行复用。
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="c">3</li>
</ul>
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="d">4</li>
</ul>
此时vue会复用前两个节点(比对后发现前两个节点都不需要更改),只需要在原来的dom元素上追加一个子元素而已。
old tail -> new tail
在头结点进行比较时,发现不是一个节点,则再次比较两个children的尾节点。
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="c">3</li>
</ul>
<ul>
<li key="b">2</li>
<li key="c">3</li>
</ul>
在头结点不同,尾节点相同的情况下,会一直比较尾节点,发现相同则复用,到下一轮循环发现头节点还是不一致,继续比对尾节点。此时页面渲染也只是会删除一个旧的dom。
交叉比对
old head -> new tail
在头结点和尾节点都不同的情况下,去比对旧vnode的头结点和新vnode的尾节点。
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="c">3</li>
</ul>
<ul>
<li key="b">2</li>
<li key="c">3</li>
<li key="a">1</li>
</ul>
比较旧vnode的头节点和新vnode的尾节点发现一样,则进行复用,只需要移动dom元素的位置到其应该在的位置即可。
此时会复用这三个节点,只是会把第一个li移动到最后。
old tail -> new head
比较旧vnode的尾节点和新vnode的头结点,一样则也会复用节点。
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="c">3</li>
</ul>
<ul>
<li key="c">3</li>
<li key="b">2</li>
<li key="a">1</li>
</ul>
此时也只是移动三个节点中key为a和c这两个dom元素的位置。
乱序比较
当前面四种情况都不符合,恭喜了,已经没办法优化了,或者说再想办法优化并不是那么划算了。因为这个时候我们已经需要拿新vnode中的每个节点,去和旧vnode中的每个节点依次比对,此时的时间复杂度已经是O(N^2)了。算是很高的复杂度了。
先根据旧节点vnode集合生成一个key和节点所在索引的map。
function makeIndexByKey(children) {
const map = {};
children.forEach((child, index) => (map[child.key] = index));
return map;
}
我们让新vnode的每个节点,都拿出key去这个map中找旧节点的索引,如果找到则可以复用,找不到则需要创建新的dom元素然后插入到指定位置;如果找到了,则移动这个节点到指定位置,并且标识当前节点已经使用。
const map = makeIndexByKey(oldChildren);
let moveIndex = map[newStartVnode.key];
if (moveIndex !== undefined) {
const moveVnode = oldChildren[moveIndex];
insertBefore(el, moveVnode, oldStartVnode.el);
oldChildren[moveIndex] = undefined;
patchVnode(moveVnode, newStartVnode);
} else {
insertBefore(el, createEle(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
此时,就完成了所有diff算法的步骤。
<ul>
<li key="a">1</li>
<li key="b">2</li>
<li key="c">3</li>
<li key="d">4</li>
</ul>
<ul>
<li key="g">6</li>
<li key="f">5</li>
<li key="h">7</li>
<li key="a">1</li>
<li key="c">3</li>
<li key="b">2</li>
</ul>
这种复杂的也能实现dom复用了。
此时对于key来说,是不能出现重复的。否则会报错。
**核心代码:**大概一百行左右吧。
function patchVnode(oldVNode, vnode) {
const el = oldVNode.el;
if (!isSameVNode(oldVNode, vnode)) {
const newEl = createEle(vnode);
replaceChild(el.parentNode, newEl, el);
return newEl;
}
vnode.el = el;
if (!oldVNode.tag) {
if (oldVNode.text !== vnode.text) {
textContent(el, vnode.text);
}
}
patchProps(el, oldVNode.props, vnode.props);
const oldChildren = oldVNode.children || [];
const newChildren = vnode.children || [];
const oldLen = oldChildren.length,
newLen = newChildren.length;
if (oldLen && newLen) {
updateChildren(el, oldChildren, newChildren);
} else if (newLen) {
mountChildren(el, newChildren);
} else if (oldLen) {
unmountChildren(el, oldChildren);
}
return el;
}
function updateChildren(el, oldChildren, newChildren) {
let oldStartIndex = 0,
oldEndIndex = oldChildren.length - 1,
newStartIndex = 0,
newEndIndex = newChildren.length - 1,
oldStartVnode = oldChildren[oldStartIndex],
oldEndVnode = oldChildren[oldEndIndex],
newStartVnode = newChildren[newStartIndex],
newEndVnode = newChildren[newEndIndex];
const map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) oldStartVnode = oldChildren[++oldStartIndex];
if (!oldEndVnode) oldEndVnode = oldChildren[--oldStartIndex];
else if (isSameVNode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVNode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
else if (isSameVNode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
console.log(oldEndVnode, newStartVnode);
insertBefore(el, oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
}
else if (isSameVNode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
insertBefore(el, oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
} else {
let moveIndex = map[newStartVnode.key];
if (moveIndex !== undefined) {
const moveVnode = oldChildren[moveIndex];
insertBefore(el, moveVnode, oldStartVnode.el);
oldChildren[moveIndex] = undefined;
patchVnode(moveVnode, newStartVnode);
} else {
insertBefore(el, createEle(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
const anchor = newChildren[newEndIndex + 1]?.el;
insertBefore(el, createEle(newChildren[i]), anchor);
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
oldChildren[i] && removeChild(el, oldChildren[i].el);
}
}
}
function makeIndexByKey(children) {
const map = {};
children.forEach((child, index) => (map[child.key] = index));
return map;
}
为什么需要key
直接将新节点替换老节点,很消耗性能,所以我们不直接替换,而是在比较两个节点之间的区别之后在替换,这就是diff算法。
diff算是 是一个平级比较的过程,父亲和父亲节点比对 儿子和儿子节点比对。
我们在比较两个虚拟dom是否一致的时候,是根据虚拟dom的标签名和key值来进行比较的。如果没有key,相当于只要标签名一致,我我们就认为这两个虚拟节点是一样的,然后判断其子元素…
当我们在遍历动态列表,给其增加key的时候,要尽量避免使用索引作为key,因为两次的虚拟dom的key都是从0开始的,可能会发生错误复用。
注意:在vue和react中,我们说的key要唯一,实际上是在同级的vnode情况下(也就是兄弟节点这些),并不意味着key需要全局唯一。
|