【手写 Vue2.x 源码】第三十一篇 - diff 算法 - 比对优化(下)
一,前言
上篇,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
等其它新增节点,需要将它们(即从newStartIndex
到newEndIndex
之间的所有节点),全部添加到老节点的儿子集合中;
代码实现:
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
比对完成后,需要将剩余新节点(E
、F
)添加到老儿子中的对应位置(当前应添加到老儿子集合的头部)
问题:如何向头部位置新增节点
问题:如何将新增节点E
、F
放置到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
就插入到了D
和A
之间)
继续C
和C
比,比对完成后继续移动指针,并移动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 算法 - 比对优化(下)相关推荐
- 前端进阶-手写Vue2.0源码(三)|技术点评
前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...
- 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化
一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...
- 【手写 Vue2.x 源码】第十八篇 - 根据 render 函数,生成 vnode
一,前言 上篇,介绍了 render 函数的生成,主要涉及以下两点: 使用 with 对生成的 code 进行一次包装 将包装后的完整 code 字符串,通过 new Function 输出为 ren ...
- 【手写 Vue2.x 源码】第十九篇 - 根据 vnode 创建真实节点
一,前言 上篇,根据 render 函数,生成 vnode,主要涉及以下几点: 封装 vm._render 返回虚拟节点 _s,_v,_c的实现 本篇,根据 vnode 虚拟节点渲染真实节点 二,根据 ...
- 【手写 Vue2.x 源码】第二十二篇 - dep 和 watcher 关联
一,前言 上篇,主要介绍了 Vue 依赖收集的过程分析: 介绍了 Vue 的响应式特性 介绍了 Vue 的依赖收集过程 介绍了 dep 和 watcher 以及观察者模式: 本篇,Vue 依赖收集的实 ...
- 【卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10)】
卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10) 在上一章已经完成了卷积神经网络的结构分析,并通过各个模块理解 ...
- android米聊手写和涂鸦源码,Android访米聊手写和涂鸦源码
Android访米聊手写和涂鸦源码 \请下载源代码,只上传Android访米聊手写和涂鸦源码源程序列表内容,如果需要此程序,请点击-下载,下载需要资料源代码. Android访米聊手写和涂鸦源码.ra ...
- android米聊手写和涂鸦源码,涂鸦手写齐上阵 新版米聊将快乐进行到底
"米聊"是由小米科技出品的一款多平台,跨移动.联通.电信运营商的手机端免费即时通讯工具,通过手机网络(WiFi.3G.GPRS),可以跟你的米聊联系人进行无限量的免费的实时的语音对 ...
- python程序实例源代码-Python 神经网络手写识别实例源码
深度学习和神经网络对很多初学者来说都是摸不着头脑,今天分享一个完整的手写识别的实例,学习和理解了这个实例代码和过程就基本上掌握了神经网络. 1.构建神经网络类 network_claas.py #!/ ...
最新文章
- 【Unity 3D】学习笔记三十六:物理引擎——刚体
- XO Wave-数字音频编纂软件
- python不能处理excel文件-python处理excel文件(xls和xlsx)
- 小宝机器人的储存容量_新iPad Pro储存越来越大:甚至到1T!您应该购买哪种存储容量呢?...
- Java读取String分行字符串
- 单片机上运行linux程序代码,在Linux下烧录51单片机
- 为解决半导体供应短缺问题,全球半导体厂商迅速增产
- winform 自定义控件属性在属性面板中显示的问题
- Linux查看磁盘是否被占满,怎么查看Linux磁盘空间是否满了?
- 【数据压缩(十)】H264文件分析2
- js加html连线实现流程,(jsPlumb开发入门教程实现html5拖拽连线.doc
- 希尔伯特变换 matlab,MATLAB的实现Hilbert变换程序_matlab
- 两节串联锂电池充电管理芯片,IC的充放电方案
- 基于Java+Springboot+Vue校园志愿者管理系统设计与实现
- 深夜的士(2019/7/19)
- 【华为昇腾社区、鹏城实验室】中国软件开源创新大赛·赛道二:任务挑战赛(模型王者挑战赛黄金赛段)
- maven配置多仓库,第一个库没有,找第二个库
- SAR-Scape处理SBAS-InSAR报错原因之一
- 【设计模式】模板模式实现报表模板
- 搜索引擎登录工具_用开源搜索引擎定制你的互联网
热门文章
- idea连接数据库的方法
- CVE-2020-14882:Weblogic Console 权限绕过深入解析
- 聊城大学计算机学院Q,聊城大学计算机学院09—10学年第1学期期末考试2008级1、3班《离散数学》试题(2份)...
- tf.nn.lrn() 局部响应归一化函数
- 论文阅读笔记——模式物种中个体的自动检测和识别
- java 可迭代对象,什么是可迭代对象(Iterable objects)?
- Build-Tools 下载不下来,自己可以手动去网页下载哦
- c++11 总结-2
- CCF之消除类游戏(java)
- 百度相册批量上传下载类