第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. C# 开发技巧 轻松监控方法执行耗时

    前言 MethodTimer.Fody 是一个功能强大的库,可以用于测量 .NET 应用程序中的方法的执行时间.允许你在不修改代码的情况下,自动地测量和记录方法的执行时间. 这个工具是基于.NET的 ...

  2. Git的存储原理

    目录 Git 设计原理 Git vs SVN Git 存储模型 .git 目录结构 Git 基本数据对象 Git 包文件 Git 引用 Git 设计原理 概括的讲,Git 就是一个基于快照的内容寻址文 ...

  3. 初学者使用1Panel面板快速搭建WordPress网站

    之前介绍了宝塔面板以及如何搭建wordpress网站,这篇文章我们来学习如何使用1Panel面板搭建wordpress网站. 一.1Panel面板介绍 1. 介绍 1Panel 是一个现代化.开源的基 ...

  4. 3、SpringBoot2之配置文件

    3.1.环境搭建 3.1.1.在project创建新module 3.1.2.选择maven 3.1.3.设置module名称和路径 3.1.4.module初始状态 3.1.5.引入springbo ...

  5. 【Vue】14 UI库

    PC端: 第一梯队:基于JQuery实现的Dom操作,和一些简单CSS样式组成 Layui Bootstrap EasyUI 第二梯队:基于Vue2.0开发的UI库,组件化开发 ElementUI A ...

  6. 国产操作系统 “银河麒麟操作系统V10” 试用失败历程

    面对外国的科技封锁,具有自主产权的国产软件已经变得迫在眉睫了,几天前在新闻上看到国产的操作"银河麒麟操作系统V10"已经发布,于是抱着尝鲜的心态想着去试着用用.虽然都是基于linu ...

  7. 什么是MMU

    一.MMU的定义   MMU是Memory Management Unit的缩写,中文名是内存管理单元,有时也称作分页内存管理单元(Paged Memory Management Unit,缩写为PM ...

  8. A. Flipping Game

    A. Flipping Game 本质上是让我们找出一段区间内\(0\)的个数大于\(1\)的个数的最多的区间,且必须进行一次操作,所以可以考虑区间\(dp\),或者最小子序列和 1 最小子序列和 \ ...

  9. 看了这几个C语言例子,你一定和我一样连说5个卧槽,声音一次比一次大

    曾经我一直以为自己C语言学的还挺好的,直到看到这几个例子. 例1 首先来看一下,大师是如何求圆周率的,一口君实在词穷,first卧槽. #include <stdio.h> long a= ...

  10. Dapr v1.14 版本已发布

    Dapr是一套开源.可移植的事件驱动型运行时,允许开发人员轻松立足云端与边缘位置运行弹性.微服务.无状态以及有状态等应用程序类型.Dapr能够确保开发人员专注于编写业务逻辑,而不必分神于解决分布式系统 ...