写点什么

【Vue2.x 源码学习】第三十篇 - diff 算法 - 比对优化(上)

用户头像
Brave
关注
发布于: 3 小时前
【Vue2.x 源码学习】第三十篇 - diff算法-比对优化(上)

一,前言


上篇,介绍了 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 算法-比对优化(下);

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第三十篇 - diff算法-比对优化(上)