第10章、双端 Diff 算法

10.1 双端比较的原理

上一章的移动算法并不是最优的,比如我们把 ABC 移动为 CAB,如下

A     C
B --> A
C B

按照上一章的算法,我们遍历新的数组,然后定下第一个元素 C 的位置后,后面的 AB 都需要被移动。但是显而易见的,我们其实可以只移动 C 移动一次即可。

而使用双端 Diff 就是记录新旧两个子数组的端点,然后 新头节点-旧头结点、新尾结点-旧尾结点、旧头结点-新尾结点、旧尾结点-新头节点,这样四种组合依次去比较,直到找到匹配的元素,然后根据新节点的位置把对应的旧节点移动。

function patchChildren(n1, n2, container) {
// ... 其他逻辑省略
if (Array.isArray(n2.children)) {
// 如果新子元素是一组节点
if (Array.isArray(n1.children)) {
patchKeyedChildren(n1, n2, container)
}
}
} function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children
const newChildren = n2.children
// 四个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 四个索引值对应的 vnode
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
// 循环执行四个判断条件
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVNode.key === newStartVNode.key) {
// 头部相同 不用移动节点 只需要patch打补丁
patch(oldStartVNode, newStartVNode, container)
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (oldEndVNode.key === newEndVNode.key) {
// 尾部相同 不用移动节点 只需要patch打补丁
patch(oldEndVNode, newEndVNode, container)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key) {
// 如果旧头结点和新尾结点key相同 先patch 再把节点移到尾部
patch(oldStartVNode, newEndVNode, container)
insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key) {
// 如果新的头结点和旧尾结点key相同 先patch 再把节点移到头部
patch(oldEndVNode, newStartVNode, container)
insert(oldEndVNode.el, container, oldStartVNode.el)
// 移动索引值
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
}
}
}

10.2 双端比较的优势

使用了上述的双端 Diff,在大部分情况下可以少移动一些节点。

10.3 非理想状况下的处理方式

理想就是说每次四种条件都有一种能够命中,实际上可能全部没有命中,比如:

新    旧
2 1
4 <-- 2
1 3
3 4

这种情况时我们就处理新节点中的第一个节点。首先在旧数组中找到 key 相同的进行 patch 没有相同的就创建新节点,然后把该节点已移动到最前面。同时把旧节点置为 undefined 然后注意循环到旧节点位空时要继续前移/后移,来忽略处理过的旧节点。

// 插到循环开始位置
if (!oldStartVNode) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (!oldEndVNode) {
oldEndVNode = oldChildren[++oldEndIdx]
}
// 忽略中间那四个判断条件
const idxInOld = oldChildren.findIndex(
(node) => node?.key === newStartVNode.key
)
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove, newStartVNode, container)
// 插到头部
insert(vnodeToMove.el, container, oldStartVNode.el)
// 注意处理完旧节点要在旧数组中置空
oldChildren[idxInOld] = undefined
newStartVNode = newChildren[newStartIdx++]
}

10.4 添加新元素

还是上面的情况,如果在旧节点中找不到匹配的 key,证明是新添加的元素,需要创建新节点,然后插入到头部。

const idxInOld = oldChildren.findIndex(
(node) => node?.key === newStartVNode.key
)
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove, newStartVNode, container)
// 插到头部
insert(vnodeToMove.el, container, oldStartVNode.el)
// 注意处理完旧节点要在旧数组中置空
oldChildren[idxInOld] = undefined
} else {
// 将新节点挂载到头部,oldStartVNode.el 作为锚点
patch(null, newStartVNode, container, oldStartVNode.el)
}
newStartVNode = newChildren[++newStartIdx]

然后在循环结束后,如果旧数组处理完了但是新数组还有剩余,证明这些节点都是新增的,需要依次创建。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...
}
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
const anchor = newChildren[newEndIdx + 1]
? newChildren[newEndIdx + 1].el
: null
patch(null, newChildren[i], container, anchor)
}
}

注意这里的 anchor 就是说我们双端 diff 的时候,如果有一些节点已经放在最后了,需要放在那些节点之前。

10.5 移除不存在的元素

如果循环完旧元素有剩余,则需要卸载。

if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
// ...
} else if (oldEndIdx >= oldStartIdx && newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
unmount(oldChildren[i])
}
}

10.6 总结

这章整体还是比较好理解的,主要之前只知道双端比较,现在更清楚了匹配上之后要怎么移动。

同时这里只判断了 key 相同,在 Vue2 源码中还判断了标签等属性。

function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}

《Vue.js 设计与实现》读书笔记 - 第10章、双端 Diff 算法的更多相关文章

  1. 【vue.js权威指南】读书笔记(第一章)

    最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...

  2. 【vue.js权威指南】读书笔记(第二章)

    [第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...

  3. 《JavaScript Dom 编程艺术》读书笔记-第10章

    用JS实现动画~内容包括: 1. 动画基础知识 2. 用动画丰富网页的浏览效果 动画就是让元素的位置随时间而不断变化. 位置: //CSSelement{ position:absolute; top ...

  4. 《C++ Primer 4th》读书笔记 第10章-关联容器

    原创文章,转载请注明出处:http://www.cnblogs.com/DayByDay/p/3936464.html

  5. 《C和指针》 读书笔记 -- 第10章 结构和联合

    1.聚合数据类型能够同时存储超过一个的单独数据,c提供了两种类型的聚合数据类型,数组和结构. 2.[1] struct SIMPLE { int a; }; struct SIMPLE x; [2] ...

  6. css权威指南读书笔记-第10章浮动和定位

    这一章看了之后真是豁然开朗,之前虽然写了圣杯布局和双飞翼布局,有些地方也是模糊的,现在打算总结之后再写一遍. 以下都是从<css权威指南>中摘抄的我认为很有用的说明. 浮动元素 一个元素浮 ...

  7. $《第一行代码:Android》读书笔记——第10章 Android网络编程

    (一)WebView的用法 1.WebView也是一个普通的控件. 2.常用用法: WebView webView = (WebView)findViewById(R.id.web_view); we ...

  8. INSPIRED启示录 读书笔记 - 第10章 管理上司

    十条经验 1.为项目波动做好准备:用项目波动代指让你心烦意乱的各种返工.计划变更.不要企图消灭项目波动,但是可以尽量降低其负面影响.方法是提高警惕,记录工作进度,掌握项目波动的规律,寻找对策.制订项目 ...

  9. 《Unix环境高级编程》读书笔记 第10章-信号

    1.引言 信号是软件中断. 信号提供了一种处理异步事件的方法. 2. 信号概念 信号的名字都是以3个字符SIG开头. Linux3.2.0支持31种信号.FreeBSD.Linux和Solaris作为 ...

  10. C++ primer plus读书笔记——第10章 对象和类

    第10章 对象和类 1. 基本类型完成了三项工作: 决定数据对象需要的内存数量: 决定如何解释内存中的位: 决定可使用数据对象执行的操作或方法. 2. 不必在类声明中使用关键字private,因为这是 ...

随机推荐

  1. 硬件开发笔记(二十八):TPS54331电源设计(一):5V电源供电原理图设计

    前言   电源供电电路设计很重要,为了更好的给对硬件设计有需求的人,特意将电源设计的基础过程描述出来.  本篇描述设计常用的12V转5V电路3A.   TPS54331(DC-DC稳压器) 概述    ...

  2. 【Java】Collection 集合框架概述

    Collection 集合框架概述 1.集合.数组都是为了存储数据而产生的 2.为什么需要集合?为了更灵活方便的存储数据,且集合能存储的容量比数组更大 3.存储的概念还停留在内存活动范围内,也只是短暂 ...

  3. (续) python 中 ctypes 的使用尝试

    内容接前文: https://www.cnblogs.com/devilmaycry812839668/p/15032493.html ================================ ...

  4. 【转载】 miniImageNet数据集介绍

    版权声明:本文为CSDN博主「miguemath」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/wangkai ...

  5. Long Way to be Non-decreasing 题解

    前言 题目链接:洛谷:CF. 题意简述 yzh 喜欢单调不降序列. 她有一个序列 \(a\),最初为 \(a_1, \ldots, a_n\),其中每个元素都在 \([1, m]\) 内. 她希望使序 ...

  6. Maven经验分享(六)Jboss热部署

    jboss7的部署方式比较多的,如果使用maven构建和管理项目,那当然是使用jboss-as-maven-plugin插件来部署项目是最方便的了. pom.xml配置如下: <plugin&g ...

  7. 如何让你的C语言程序打印的log多一点色彩?(超级实用)

    接着上一篇文章<由字节对齐引发的一场"血案" > 在平常的调试中,printf字体格式与颜色都是默认一致的. 如果可以根据log信息的重要程度,配以不同的颜色与格式,可 ...

  8. 默认情况下,CentOS 7上MySQL / MariaDB的存储位置在哪里?

    Where is MySQL / MariaDB storage location by default on CentOS 7? No special configuration to the Ma ...

  9. VSCode 插件离线安装方法

    一.引言 最近想要使用 VSCode 来进行项目的开发工作,无奈工作机上无法上网.这就涉及到了相关插件的离线安装的问题. 在参考了 https://blog.csdn.net/wangwei703/a ...

  10. 深度解析HarmonyOS SDK实况窗服务源码,Get不同场景下的多种模板

    HarmonyOS SDK实况窗服务(Live View Kit)作为一个实时呈现应用服务信息变化的小窗口,遍布于设备的各个使用界面,它的魅力在于将复杂的应用场景信息简洁提炼并实时刷新,在不影响当前其 ...