一,前言

上篇,diff 算法-比对优化(上),主要涉及以下几个点:

  • 介绍了如何对儿子节点进行比对;
  • 新老儿子节点可能存在的 3 种情况及代码实现;
  • 新老节点都有儿子时,diff 的方案介绍与处理逻辑分析;

本篇,diff 算法-比对优化(下)


二,比对优化

1,前文回顾

上一篇提到,新老儿子节点比对可能存在的 3 种情况及对应的处理方法:

  • 情况 1:老的有儿子,新的没有儿子

    处理方法:直接将多余的老dom元素删除即可;

  • 情况 2:老的没有儿子,新的有儿子

    处理方法:直接将新的儿子节点放入对应的老节点中即可;

  • 情况 3:新老都有儿子

    处理方法:执行diff比对,即:乱序比对;

针对情况 3 新老儿子节点的比对,采用了“头尾双指针”的方法,如图所示:

优先对新老儿子节点的“头头、尾尾、头尾、尾头”节点进行比对,若均未能命中,最后再执行乱序比对;

2,节点比对的结束条件

结束条件:直至新老节点一方遍历完成,比对才结束;

即:“老的头指针和尾指针重合"或"新的头指针和尾指针重合”;

此时,由于发生"老的头指针和尾指针重合",比对结束,图上状态便是循环中的最后一次比对;

新老节点比对完成后,可复用节点已识别完成,老节点中A、B、C、D节点被复用;将新增节点E添加到老的节点中即可;

将上述逻辑框架转化为代码实现,如下:

// src/vdom/patch.js/*** 新老都有儿子时,执行乱序比对,即 diff 算法的核心逻辑* 备注:采用头尾双指针的方式;对头头、尾尾、头尾、尾头 4 种特殊情况做优化;** @param {*} el * @param {*} oldChildren  老的儿子节点* @param {*} newChildren  新的儿子节点*/
function updateChildren(el, oldChildren, newChildren) {// 声明头尾指针(老)let oldStartIndex = 0;let oldStartVnode = oldChildren[0];let oldEndIndex = oldChildren.length - 1;let oldEndVnode = oldChildren[oldEndIndex];// 声明头尾指针(新)let newStartIndex = 0;let newStartVnode = newChildren[0];let newEndIndex = newChildren.length - 1;let newEndVnode = newChildren[newEndIndex];// while 循环的中止条件:新老其中一方遍历完成即为结束;// 即"老的头指针和尾指针重合"或"新的头指针和尾指针重合" while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){// 1,优先做4种特殊情况比对:头头、尾尾、头尾、尾头// 2,若未能命中,则采用 diff 乱序比对// 3,比对完成后移动指针,继续下一轮比对,直至比对完成}// 比对完成后,继续处理剩余节点...
}

备注:由于diff算法采用了while循环进行处理,所以复杂度为O(n);

3,情况 1:新儿子比老儿子多,插入新增的

情况 1:新儿子比老儿子多,又分为“从头部开始移动指针”和“从尾部部开始移动指针”两种情况;

从头部开始移动指针

头头比对:

第一次匹配,匹配后移动新老头指针:

第二次匹配,匹配后移动新老头指针:

通过多次比对后,直至老节点的头尾指针发生重合,此时,D节点就是while循环的最后一次比对:

本次比对完成之后,指针会继续向后移动一次,将导致老节点的头指针越过尾指针,此时,while循环结束;

while循环结束时的指针状态如下:

此时,新节点的头指针指向的节点E为新增节点,后面可能还存在F、G、H等其它新增节点,需要将它们(即从newStartIndexnewEndIndex之间的所有节点),全部添加到老节点的儿子集合中;

代码实现:

while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){ // 头头比对:if(isSameVnode(oldStartVnode, newStartVnode)){ // ******* 1,比对新老虚拟节点 *******// isSameVnode 只能判断标签和 key 一样,但属性可能还有不同;// 因此,继续调用 patch 方法,对新老虚拟节点中的属性做递归更新操作;patch(oldStartVnode, newStartVnode); // ******* 2,比对完成后,更新指针和节点位置 *******oldStartVnode = oldStartVnode[++oldStartIndex]; // 更新老的头指针和头节点newStartVnode = newStartVnode[++newStartIndex]; // 更新新的头指针和头节点}
}// 新的节点有多余时,追加到 dom 中
if(newStartIndex <= newEndIndex){ // 遍历多余节点:新的开始指针和新的结束指针之间的节点for(let i = newStartIndex; i <= newEndIndex; i++){ // 获取虚拟节点并生成真实节点,添加到 dom 中 el.appendChild(createElm(newChildren[i])) }
}

isSameVnode方法,只能判断标签和 key 是否完全一样,无法判断属性变化;

因此,需要继续通过 patch 方法,对新老虚拟节点中的属性进行递归更新操作;

测试效果:对复用节点做属性更新,并添加新节点

let render1 = compileToFunction(`<div><li key="A">A</li><li key="B">B</li><li key="C">C</li><li key="D">D</li>
</div>`);// 对复用节点做属性更新,并添加新节点
let render2 = compileToFunction(`<div><li key="A" style="color:red">A</li><li key="B" style="color:blue">B</li><li key="C" style="color:yellow">C</li><li key="D" style="color:pink">D</li><li key="E">E</li><li key="F">F</li>
</div>`);

更新前:

更新后:

执行结果:A、B、C、D节点被复用并更新了样式属性,且继续添加了新节点;

即:尽可能复用原有节点,仅更新需要更新的部分;

问题:

  • 将新儿子中的新增节点直接追加到老儿子集合中,使用appendChild即可;
  • 但是,如果新增的节点在头部位置,就不能用appendChild了,看下面的尾尾比对分析;

从尾部开始移动指针

尾尾比对:

尾指针向前移动,当老节点的头尾指针重合,即while循环的最后一次比对:

比对完成指针向前移动后,循环结束时的指针状态如下:

while比对完成后,需要将剩余新节点(EF)添加到老儿子中的对应位置(当前应添加到老儿子集合的头部)

问题:如何向头部位置新增节点

问题:如何将新增节点EF放置到A前面?

分析:

  • 首先,想要添加到A节点的前面,就不能再使用appendChild做向后追加操作了;
  • 前面的代码是指“从新的头指针到新的尾指针”这一区间的节点,即for (let i = newStartIndex; i <= newEndIndex; i++) 所以,从处理顺序上,是先处理E节点,再处理F节点

先处理E节点:将E节点放置到A节点前的位置:

再处理F节点:将F节点插入到A节点与E节点之间的位置:

这样,当新增区域的头尾指针重合,即为最后一次比对;

方案设计:两种比对方式的合并处理

新增的节点的两种情况:有可能被追加到后面,也有可能被插入到前面:

  • 头头比较时,将新增节点追加到老儿子集合的尾部;
  • 尾尾比较时,将新增加点添加到老儿子集合的头部;

综合以上两种情况,如何确定向前 or 向后添加节点呢?

这取决于while循环结束时,新儿子集合的尾指针newChildren[newEndIndex + 1]上是否存在节点:

  • 如果无节点:说明是从头向尾进行比对的,新增节点需要被追加到老儿子集合后面,使用appendChild直接追加即可;
  • 如果有节点:说明是从尾向头进行比对的,新增节点需要被添加到老儿子集合前面,使用insertBefore插入指定到位置;

以上分析对应的代码实现,如下:

// 1,新的多(以新指针为参照),插入新增节点
if (newStartIndex <= newEndIndex) {// ****** 先获取参照物 ******// 判断当前尾节点的下一个元素是否存在://  1,如果存在:说明是尾尾比对,插入到当前尾节点下一个元素前面;//  2,如果不存在(下一个是 null):说明是头头比对,追加即可// 取参考节点 anchor:决定新节点放置到前边还是后边//  逻辑:取 newChildren 的尾部 +1,判断是否为 null//  解释:若有值则说明是向前移动,取出当前虚拟元素的真实节点 el,并将新节点添加到此真实节点之前// (由于是向前移动比对,故此虚拟元素在前一次比对中,已经复用了老节点 el,所以直接取新的虚拟节点上的 el 即可)let anchor = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el// ****** 再根据参照物进行处理 ******// 遍历多出的节点:新的开始指针和新的结束指针之间的节点for (let i = newStartIndex; i <= newEndIndex; i++) {// 获取对应的虚拟节点,并生成真实节点,添加到 dom 中// el.appendChild(createElm(newChildren[i]))// 逻辑合并:将 appendChild 更换为 insertBefore 处理//  效果:既有 appendChild 又有 insertBefore 功能,直接放入参考节点即可;//  解释:对于 insertBefore 方法,如果 anchor=null,就等同于appendChild;如果有值,则是 insertBefore;el.insertBefore(createElm(newChildren[i]), anchor)}
}

这里非常重要的一个思想,就是找到参考节点anchor;之后,再将新的真实节点放置于参考节点之前即可;

注意此处insertBefore方法的妙用:

  • 当第二个参数为null时,效果等同于appendChild追加;
  • 当第二个参数不为null时,效果是insertBefore插入;

所以,同时具备了appendChild追加和insertBefore插入的效果;

以上,对新节点比老节点多的两种情况,分别进行了处理;

除此之外,还可能存在老节点比新节点多的情况,那么,该如何处理呢?


4,情况 2:老儿子比新儿子多,删除多余

代码示例: 老儿子比新儿子多

let render1 = compileToFunction(`<div><li key="A" style="color:red">A</li><li key="B" style="color:blue">B</li><li key="C" style="color:yellow">C</li><li key="D" style="color:pink">D</li>
</div>`);let render2 = compileToFunction(`<div><li key="A" style="color:red">A</li><li key="B" style="color:blue">B</li><li key="C" style="color:yellow">C</li>
</div>`);

如图:

老的比新的多,在移动过程中就会出现:新的已经到头了,但老的还有;

所以,当移动结束时:老的头指针会和尾指针重合,新的头指针会越过新的尾指针;

代码实现:

将老儿子集合“从头指针到尾指针”区域中,多余的真实节点删除:

// 2,老儿子比新儿子多,(以旧指针为参照)删除多余的真实节点
if(oldStartIndex <= oldEndIndex){for(let i = oldStartIndex; i <= oldEndIndex; i++){let child = oldChildren[i];el.removeChild(child.el);}
}

5,情况 3:反序情况(头尾、尾头)

反序情况:如图,新老儿子集合中的节点顺序是完全相反的;

这种情况下,可以使用“老的头指针”和“新的尾指针”进行比较,即“头尾比对”

每次比较完成后,“老的头指针”向后移动,“新的尾指针”向前移动;并在比对完成后,直接将老节点A放置到老节点集合的最后:

更确切的说,应该是插入到尾指针的下一个节点(null)之前;

(在移动前,想象尾指针指向的D节点后面,还存在着下一个节点为null

说明:

js本身是无法做到“向一个元素之后添加一个元素”的;比如:appendChild 是向最后进行追加;

因此,在逻辑上,只能是先找到目标元素的下一个元素,再向下一个元素之前添加一个新的元素;


继续比对B,比对完成后继续移动指针,并移动B,插入到尾指针的下一个节点之前(这时尾指针D的下一个节点,边是上一次移动过来的A,所以B就插入到了DA之间)

继续CC比,比对完成后继续移动指针,并移动C,插入到尾指针的下一个节点之前(这时,尾指针D的下一个是上一次移动过来的B

接下来继续比对D,此时,就会发现“旧的头指针”和“新的头指针”都指向了D;

这时,就比对完成了,D无需再移动,结果就是D C B A

(整个反序过程,共移动了3 次,只对节点进行了移动操作,并没有创建新节点)

结论:对于反序操作来说,需要去比对头尾指针(老的头和新的尾),每次比对完成之后,头指针向右移动,尾指针向左移动;

代码实现,添加“头尾比较”逻辑:

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {if (isSameVnode(oldStartVnode, newStartVnode)) {patch(oldStartVnode, newStartVnode);oldStartVnode = oldChildren[++oldStartIndex];newStartVnode = newChildren[++newStartIndex];}else if(isSameVnode(oldEndVnode, newEndVnode)){patch(oldEndVnode, newEndVnode);oldEndVnode = oldChildren[--oldEndIndex];newEndVnode = newChildren[--newEndIndex];// 头尾比较:老的头节点和新的尾节点做对比}else if(isSameVnode(oldStartVnode, newEndVnode)){// patch 方法只会 diff 比较并更新属性,但元素的位置不会变化patch(oldStartVnode, newEndVnode); // diff:会递归比对儿子// 移动节点:将当前的节点插入到最后一个节点的下一个节点的前面去el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);// 指针置位--下一次循环继续处理oldStartVnode = oldChildren[++oldStartIndex];newEndVnode = newChildren[--newEndIndex];}
}

注意:

由于dom具有移动性,appendChild、insertBefore操作都会使dom产生移动效果;

在做指针置位前,必须先完成节点的插入操作,之后才能移动指针,否则原来的dom就会被移走;

测试效果:

更新前:老节点ABCD均没有样式属性;

更新后:老节点ABCD被复用,并添加了对应的样式属性;

同理,尾头比对的代码实现:

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {if (isSameVnode(oldStartVnode, newStartVnode)) {patch(oldStartVnode, newStartVnode);oldStartVnode = oldChildren[++oldStartIndex];newStartVnode = newChildren[++newStartIndex];}else if(isSameVnode(oldEndVnode, newEndVnode)){patch(oldEndVnode, newEndVnode);oldEndVnode = oldChildren[--oldEndIndex];newEndVnode = newChildren[--newEndIndex];}else if(isSameVnode(oldStartVnode, newEndVnode)){patch(oldStartVnode, newEndVnode);el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);oldStartVnode = oldChildren[++oldStartIndex];newEndVnode = newChildren[--newEndIndex];// 尾头比较}else if(isSameVnode(oldEndVnode, newStartVnode)){patch(oldEndVnode, newStartVnode);  // patch方法只会更新属性,元素的位置不会变化// 移动节点:将老的尾节点移动到老的头节点前面去el.insertBefore(oldEndVnode.el, oldStartVnode.el);// 将尾部插入到头部// 移动指针oldEndVnode = oldChildren[--oldEndIndex];newStartVnode = newChildren[++newStartIndex];}
}

测试效果:

let render1 = compileToFunction(`<div><li key="E">E</li><li key="A">A</li><li key="B">B</li><li key="C">C</li><li key="D">D</li>
</div>`);let render2 = compileToFunction(`<div><li key="D" style="color:pink">D</li><li key="C" style="color:yellow">C</li><li key="B" style="color:blue">B</li><li key="A" style="color:red">A</li>
</div>`);

更新前:老节点ABCD均没有样式属性;

更新后:老节点ABCD被复用,并添加了对应的样式属性,老节点E被删除


三,结尾

本篇,diff 算法 - 比对优化(下),主要涉及以下几个点:

  • 介绍了儿子节点比较的流程;
  • 介绍并实现了头头、尾尾、头尾、尾头4种特殊情况比对;

下篇,diff算法 - 乱序比对;


维护日志

  • 20210805:

    • 添加了“从尾部开始移动指针”的图示
    • 添加了“问题:如何向头部位置新增节点”
    • 修改了部分有问题的图示
    • 修改了几处语义表达不够准确的地方
  • 20230219:
    • 更新了文章目录结构;
    • 添加了大量内容说明和代码注释,使比对过程清晰易懂;
    • 添加了文章内容中的代码和关键字高亮;
    • 更新了文章摘要;
  • 20230221:
    • 调整部分描述,使表达更加准确易懂;

【手写 Vue2.x 源码】第三十一篇 - diff 算法 - 比对优化(下)相关推荐

  1. 前端进阶-手写Vue2.0源码(三)|技术点评

    前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...

  2. 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化

    一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...

  3. 【手写 Vue2.x 源码】第十八篇 - 根据 render 函数,生成 vnode

    一,前言 上篇,介绍了 render 函数的生成,主要涉及以下两点: 使用 with 对生成的 code 进行一次包装 将包装后的完整 code 字符串,通过 new Function 输出为 ren ...

  4. 【手写 Vue2.x 源码】第十九篇 - 根据 vnode 创建真实节点

    一,前言 上篇,根据 render 函数,生成 vnode,主要涉及以下几点: 封装 vm._render 返回虚拟节点 _s,_v,_c的实现 本篇,根据 vnode 虚拟节点渲染真实节点 二,根据 ...

  5. 【手写 Vue2.x 源码】第二十二篇 - dep 和 watcher 关联

    一,前言 上篇,主要介绍了 Vue 依赖收集的过程分析: 介绍了 Vue 的响应式特性 介绍了 Vue 的依赖收集过程 介绍了 dep 和 watcher 以及观察者模式: 本篇,Vue 依赖收集的实 ...

  6. 【卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10)】

    卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10) 在上一章已经完成了卷积神经网络的结构分析,并通过各个模块理解 ...

  7. android米聊手写和涂鸦源码,Android访米聊手写和涂鸦源码

    Android访米聊手写和涂鸦源码 \请下载源代码,只上传Android访米聊手写和涂鸦源码源程序列表内容,如果需要此程序,请点击-下载,下载需要资料源代码. Android访米聊手写和涂鸦源码.ra ...

  8. android米聊手写和涂鸦源码,涂鸦手写齐上阵 新版米聊将快乐进行到底

    "米聊"是由小米科技出品的一款多平台,跨移动.联通.电信运营商的手机端免费即时通讯工具,通过手机网络(WiFi.3G.GPRS),可以跟你的米聊联系人进行无限量的免费的实时的语音对 ...

  9. python程序实例源代码-Python 神经网络手写识别实例源码

    深度学习和神经网络对很多初学者来说都是摸不着头脑,今天分享一个完整的手写识别的实例,学习和理解了这个实例代码和过程就基本上掌握了神经网络. 1.构建神经网络类 network_claas.py #!/ ...

最新文章

  1. 【Unity 3D】学习笔记三十六:物理引擎——刚体
  2. XO Wave-数字音频编纂软件
  3. python不能处理excel文件-python处理excel文件(xls和xlsx)
  4. 小宝机器人的储存容量_新iPad Pro储存越来越大:甚至到1T!您应该购买哪种存储容量呢?...
  5. Java读取String分行字符串
  6. 单片机上运行linux程序代码,在Linux下烧录51单片机
  7. 为解决半导体供应短缺问题,全球半导体厂商迅速增产
  8. winform 自定义控件属性在属性面板中显示的问题
  9. Linux查看磁盘是否被占满,怎么查看Linux磁盘空间是否满了?
  10. 【数据压缩(十)】H264文件分析2
  11. js加html连线实现流程,(jsPlumb开发入门教程实现html5拖拽连线.doc
  12. 希尔伯特变换 matlab,MATLAB的实现Hilbert变换程序_matlab
  13. 两节串联锂电池充电管理芯片,IC的充放电方案
  14. 基于Java+Springboot+Vue校园志愿者管理系统设计与实现
  15. 深夜的士(2019/7/19)
  16. 【华为昇腾社区、鹏城实验室】中国软件开源创新大赛·赛道二:任务挑战赛(模型王者挑战赛黄金赛段)
  17. maven配置多仓库,第一个库没有,找第二个库
  18. SAR-Scape处理SBAS-InSAR报错原因之一
  19. 【设计模式】模板模式实现报表模板
  20. 搜索引擎登录工具_用开源搜索引擎定制你的互联网

热门文章

  1. idea连接数据库的方法
  2. CVE-2020-14882:Weblogic Console 权限绕过深入解析
  3. 聊城大学计算机学院Q,聊城大学计算机学院09—10学年第1学期期末考试2008级1、3班《离散数学》试题(2份)...
  4. tf.nn.lrn() 局部响应归一化函数
  5. 论文阅读笔记——模式物种中个体的自动检测和识别
  6. java 可迭代对象,什么是可迭代对象(Iterable objects)?
  7. Build-Tools 下载不下来,自己可以手动去网页下载哦
  8. c++11 总结-2
  9. CCF之消除类游戏(java)
  10. 百度相册批量上传下载类