一,前言
上篇,介绍了diff算法-节点比对,主要涉及以下几点:
- 介绍了 diff 算法、对比方式、节点复用
- 实现了外层节点的 diff 算法
- 不同节点如何做替换更新
- 相同节点如何做复用更新:文本、元素、样式处理
本篇,diff算法-比对优化
二,比对儿子节点
1,前文回顾
上篇,通过构建两个虚拟节点来模拟 v-if 的效果,通过 patch 方法比对实现了外层节点的复用
let vm1 = new Vue({
data() {
return { name: 'Brave' }
}
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);
let vm2 = new Vue({
data() {
return { name: 'BraveWang' }
}
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
patch(oldVnode, newVnode);
}, 1000);
执行结果:
初始化时为蓝色文本
更新后变为红色文本
发现问题:
仅更新了外层 div 的 style,但 name 并没有更新为 BraveWang,
即只做了第一层节点的比对和属性更新,没有进行深层的 diff 比对
2,如何比对儿子节点
把“新的儿子节点”和“老的儿子节点”都拿出来,依次进行比对
//src/vdom/patch.js
/**
* 将虚拟节点转为真实节点后插入到元素中
* @param {*} el 当前真实元素 id#app
* @param {*} vnode 虚拟节点
* @returns 新的真实元素
*/
export function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if(isRealElement){
// 1,根据虚拟节点创建真实节点
const elm = createElm(vnode);
// 2,使用真实节点替换掉老节点
const parentNode = oldVnode.parentNode;
parentNode.insertBefore(elm, oldVnode.nextSibling);
parentNode.removeChild(oldVnode);
return elm;
}else{// diff:新老虚拟节点比对
if(!isSameVnode(oldVnode, vnode)){
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}
let el = vnode.el = oldVnode.el;
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
return el.textContent = vnode.text;
}else{
return;
}
}
updateProperties(vnode, oldVnode.data);
// TODO:比较儿子节点...
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
}
}
3,新老儿子节点的几种情况
- 情况 1:老的有儿子,新的没有儿子
- 情况 2:老的没有儿子,新的有儿子
- 情况 3:新老都有儿子
情况 1:老的有儿子,新的没有儿子
处理方法:直接将多余的老 dom 元素删除即可
// src/vdom/patch.js#patch
...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
// 情况 1:老的有儿子,新的没有儿子;直接将多余的老 dom 元素删除即可;
if(oldChildren.length > 0 && newChildren.length == 0){
// 更好的处理:由于子节点中可能包含组件,需要封装removeChildNodes方法,将子节点全部删掉
el.innerHTML = '';// 暴力写法直接清空;
}
备注:这里直接清空innerHTML是暴力写法;由于子节点中可能包含组件,所以更好的处理方式是封装一个 removeChildNodes 方法,用于删掉全部子节点
测试方法:
let vm1 = new Vue({
data() {
return { name: 'Brave' }
}
})
let render1 = compileToFunction('<div style="color:blue">{{name}}</div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);
let vm2 = new Vue({
data() {
return { name: 'BraveWang' }
}
})
let render2 = compileToFunction('<div style="color:red"></div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
patch(oldVnode, newVnode);
}, 1000);
情况 2:老的没有儿子,新的有儿子
处理方法:直接将新的儿子节点放入对应的老节点中即可;
//src/vdom/patch.js#patch
...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
// 情况 1:老的有儿子,新的没有儿子;直接将多余的老 dom 元素删除即可;
if(oldChildren.length > 0 && newChildren.length == 0){
el.innerHTML = '';
// 情况 2:老的没有儿子,新的有儿子;直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
newChildren.forEach((child)=>{// 注意:这里的child是虚拟节点,需要变为真实节点
let childElm = createElm(child); // 根据新的虚拟节点,创建一个真实节点
el.appendChild(childElm);// 将生成的真实节点,放入 dom
})
}
备注:newChildren中的child为虚拟节点,需要先通过createElm(child)创建为真实节点
测试:
let vm1 = new Vue({
data() {
return { name: 'Brave' }
}
})
let render1 = compileToFunction('<div style="color:blue"></div>');
let oldVnode = render1.call(vm1)
let el1 = createElm(oldVnode);
document.body.appendChild(el1);
let vm2 = new Vue({
data() {
return { name: 'BraveWang' }
}
})
let render2 = compileToFunction('<div style="color:red">{{name}}</div>');
let newVnode = render2.call(vm2);
setTimeout(() => {
patch(oldVnode, newVnode);
}, 1000);
情况 3:新老都有儿子
处理方法:进行 diff 比对
// src/vdom/patch.js#patch
...
// 比较儿子节点
let oldChildren = oldVnode.children || {};
let newChildren = vnode.children || {};
// 情况 1:老的有儿子,新的没有儿子;直接将对于的老 dom 元素干掉即可;
if(oldChildren.length > 0 && newChildren.length == 0){
el.innerHTML = '';
// 情况 2:老的没有儿子,新的有儿子;直接将新的儿子节点放入对应的老节点中即可
}else if(oldChildren.length == 0 && newChildren.length > 0){
newChildren.forEach((child)=>{
let childElm = createElm(child);
el.appendChild(childElm);
})
// 情况 3:新老都有儿子
}else{
// diff 比对的核心逻辑
updateChildren(el, oldChildren, newChildren);
}
这里对“老的有儿子,新的没有儿子”和“老的没有儿子,新的有儿子”两种特殊情况做了特殊的处理
接下来,当新老节点都有儿子时,就必须进行 diff 比对了;
所以,updateChildren 才是 diff 算法的核心
三,新老儿子 diff 比对的核心逻辑 updateChildren 方法
1,新老儿子 diff 比对方案介绍
继续,当新老节点都有儿子时,就需要对新老儿子节点进行比对了
新老节点的比对方案是:采用头尾双指针的方式,进行新老虚拟节点的依次比对
每次节点比对完成,如果是头节点就向后移动指针,尾节点就向前移动指针;
直至一方遍历完成,比对才结束;
即:“老的头指针和尾指针重合"或"新的头指针和尾指针重合”;
这里,为了能够提升diff算法的性能,并不会直接全部采用最耗性能的“乱序比对”
而是结合了日常使用场景,优先对4种特殊情况进行了特殊的除了:头头、尾尾、头尾、尾头
- 头和头比较,将头指针向后移动;
- 尾和尾比较,将尾指针向前移动;
- 头和尾比较,将头指针向后移动,尾指针向前移动;
- 尾和尾比较,将尾指针向后移动,头指针向前移动;
每次比对时,优先进行头头、尾尾、头尾、尾头的比对尝试,如果都没有命中才会进行乱序比较
2,diff 比对的几种特殊情况(头头、尾尾、头尾、尾头)
备注:由于 4 种情况需要画图说明,单独一篇:第三十一篇 - diff算法-比对优化(下)
除了这 4 钟特殊情况外,就只能进行乱序比对了
虽然是做乱序比对,但目标依然是最大程度实现节点复用,提升渲染性能;
备注:乱序比对如何进行节点复用,单独一篇:第三十二篇 - diff算法-乱序比对
四,结尾
本篇,diff算法-比对优化(上),主要涉及以下几个点:
- 介绍了如何进行儿子节点比对;
- 新老儿子节点可能存在的3种情况及代码实现;
- 新老节点都有儿子时的 diff 方案介绍与处理逻辑分析;
下篇,diff算法-比对优化(下)