第11章、快速 Diff 算法

11.1 相同的前置元素和后置元素

快速 Diff 算法包含预处理步骤,这借鉴了纯文本 Diff 算法的思路。

先把相同的前缀后缀进行处理,然后再比较中间部分。

function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children
const newChildren = n2.children
let j = 0
let oldVNode = oldChildren[j]
let newVNode = newChildren[j]
while (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
j++
oldVNode = oldChildren[j]
newVNode = newChildren[j]
}
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
while (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
oldEnd--
newEnd--
oldVNode = oldChildren[oldEnd]
newVNode = newChildren[newEnd]
}
}

如果旧节点都被处理完了,新节点还有剩余,证明这些都是新增的,需要依次挂载。而如果是旧节点有剩余,则需要全部卸载。

if (j > oldEnd && j <= newEnd) {
const anchorIndex = oldEnd + 1
const anchor = oldChildren[anchorIndex]
? oldChildren[anchorIndex].el
: null
while (j <= newEnd) {
patch(null, newChildren[j++], container, anchor)
}
} else if (j > newEnd && j <= oldEnd) {
while (j <= oldEnd) {
unmount(oldChildren[j++])
}
}

11.2 判断是否需要进行 DOM 移动操作

如果前缀后缀处理完之后,并没有任何一组节点被处理完,则需要进行移动操作。我们根据 key 判断相同节点,然后找到每一个新节点在旧节点中的位置,我们把这个数组存为 source

function patchKeyedChildren(n1, n2, container) {
// ...
if (j > oldEnd && j <= newEnd) {
// ...
} else if (j > newEnd && j <= oldEnd) {
// ...
} else {
const count = newEnd - j + 1 // 新节点剩余的数量
const source = new Array(count)
source.fill(-1) // 初始值全部设为 -1
const oldStart = j
const newStart = j for (let i = oldStart; i <= oldEnd; i++) {
const oldVNode = oldChildren[i]
for (let k = newStart; k <= newEnd; k++) {
const newVNode = newChildren[k]
if (oldVNode.key === newVNode.key) {
patch(oldVNode, newVNode, container)
source[k - newStart] = i
}
}
}
}
}

为了在后面操作中快速找到相同 key 的节点(而不是每次都需要遍历)可以使用 Map 存储 key 对应的节点位置。而对于找不到对于 key 的节点,则需要卸载。

const count = newEnd - j + 1 // 新节点剩余的数量
const source = new Array(count)
source.fill(-1) // 初始值全部设为 -1
const oldStart = j
const newStart = j
// 对新数组构建 key->index 索引表
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i
} for (let i = oldStart; i <= oldEnd; i++) {
const oldVNode = oldChildren[i]
const k = keyIndex[oldVNode.key]
if (typeof k !== 'undefined') {
const newVNode = newChildren[k]
patch(oldVNode, newVNode, container)
source[k - newStart] = i
} else {
unmount(oldVNode)
}
}

然后我们使用第九章类似的思路来判断是否有节点需要移动。同时记录 patch 过的节点数量,当 patch 过的元素等于新节点的数量,剩下的节点直接卸载。

const count = newEnd - j + 1 // 新节点剩余的数量
const source = new Array(count)
source.fill(-1) // 初始值全部设为 -1
const oldStart = j
const newStart = j
let moved = false
let pos = 0
// 对新数组构建 key->index 索引表
const keyIndex = {}
for (let i = newStart; i <= newEnd; i++) {
keyIndex[newChildren[i].key] = i
}
let patched = 0
for (let i = oldStart; i <= oldEnd; i++) {
const oldVNode = oldChildren[i]
if (patched <= count) {
const k = keyIndex[oldVNode.key]
if (typeof k !== 'undefined') {
const newVNode = newChildren[k]
patch(oldVNode, newVNode, container)
patched++
source[k - newStart] = i
if (k < pos) {
moved = true
} else {
pos = k
}
} else {
unmount(oldVNode)
}
} else {
// 如果更新过的节点数量已经大于新的节点数量 说明剩下的节点都需要被卸载
unmount(oldVNode)
}
}

11.3 如何移动元素

我们上面通过 moved = true 标识了需要移动,下面该考虑如何移动。

我们先计算 source 的最长递增子序列。这部分不是重点,折叠了,但是我加了下注释。应该是力扣原题~

点击查看代码
function getSequence(arr) {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
// 二分找到第一个大于arrI的位置
// u最小值 v最大值
while (u < v) {
c = ((u + v) / 2) | 0 // 取中间值
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
// 3 4 1
// 我们开始会存储 3 4 <0,1>
// 然后会存为 1 4 <2,1>
// 但是要知道 4 在 1 之前 不可以这样构造子序列
// 所以用p记录:一个下标被放入result时 它的前一个位置是哪个下标
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
} console.log(getSequence([1, 2, 3])) // [ 0, 1, 2 ]
console.log(getSequence([3, 4, 1])) // [ 0, 1 ]
console.log(getSequence([3, 2, 1])) // [ 2 ]

我们通过 getSequence 计算出最长递增子序列对应的下标数组。对于最长递增子序列中包含的下标对应的节点,不进行移动,对其他节点进行移动。

方法就是从子节点数组和 lis 数组的最后一个节点作比较,如果子节点的下标等于 lis 的值,就不移动节点,同时下标向前移动,否则移动节点。

if (moved) {
const seq = lis(source)
let s = seq.length - 1
for (let i = count - 1; i >= 0; i--) {
if (source[i] === -1) {
// 新节点
const pos = i + newStart
const newVNode = newChildren[pos]
const nextPos = pos + 1
const anchor =
nextPos < newChildren.length ? newChildren[nextPos].el : null
patch(null, newVNode, container, anchor)
}
if (i !== seq[s]) {
// 移动
const pos = i + newStart
const newVNode = newChildren[pos]
const nextPos = pos + 1
const anchor =
nextPos < newChildren.length ? newChildren[nextPos].el : null
insert(newVNode.el, container, anchor)
} else {
s--
}
}
} else {
for (let i = count - 1; i >= 0; i--) {
if (source[i] === -1) {
// 新节点
const pos = i + newStart
const newVNode = newChildren[pos]
const nextPos = pos + 1
const anchor =
nextPos < newChildren.length ? newChildren[nextPos].el : null
patch(null, newVNode, container, anchor)
}
}
}

P.S.书上没有写 moved=false 相关逻辑,我看了下还是应该写的。

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

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

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

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

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

  3. C++ primer plus读书笔记——第11章 使用类

    第11章 使用类 1. 运算符重载是一种形式的C++多态. 2. 不要返回指向局部变量或临时对象的引用.函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据. 3. 运算符重载的格式如下: ...

  4. 《Unix环境高级编程》读书笔记 第11章-线程

    1. 引言 了解如何使用多个控制线程在单进程环境中执行多个任务. 不管在什么情况下,只要单个资源需要在多个用户键共享,就必须处理一致性问题. 2. 线程概念 典型的Unix进程可以看成只有一个控制线程 ...

  5. 《C和指针》 读书笔记 -- 第11章 动态内存分配

    1.C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放,这些函数维护一个可用内存池. void *malloc(size_t size);//返回指向分配的内存块起始位置的 ...

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

    本章简单介绍了HTML5,并推荐了一个好工具Modernizr,用于检测浏览器可能支持的各种特性. HTML5的新特性包括: 可以用来在文档中绘制矢量及位图的<canvas>元素: 可以在 ...

  7. INSPIRED启示录 读书笔记 - 第11章 评估产品机会

    市场需求文档 大多数的公司产品选择权是由高管.市场部门.开发团队甚至是大客户,在这种情况下公司会跳过市场需求文档或是误写成产品规范文档,回避评估产品机会 在正常情况下,应该是由业务人员会撰写一份论证产 ...

  8. Java 核心读书笔记 第11章

    1. 异常 用户希望在出现错误时,程序能够采用一些理智的行为. 如果由于出现错误而使得某些操作无法完成,程序应该:  返回到一种安全状态,并能够进行一些其他的命令: 或者:允许用于保存所有操作的结果, ...

  9. Linux内核设计与实现 读书笔记 转

    Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...

  10. 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记

    先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...

随机推荐

  1. C#开源、简单易用的Dapper扩展类库 - Dommel

    前言 今天大姚给大家分享一个C#开源(MIT License).免费.简单易用的Dapper扩展类库,帮助.NET开发者使用Dapper的CRUD操作变得更简单:Dommel. 项目特性 Dommel ...

  2. 【Eclipse】入门使用

    Eclipse界面简单概述 第一次启动时,工作空间的选择 工作界面的介绍: 选项条 工具栏 工程浏览窗口 工程大纲窗口 控制台输出窗口 在窗口选项中悬浮放在Show View选项中可以查看所有的窗口 ...

  3. 暑假自学Java进度总结04

    一.今日所学: 1.下载并使用idea开发工具 1>了解idea的发展历史 2>尝试用idea编写代码 3>学习idea中的项目和模块操作 2.学习赋值运算符 加后赋值:" ...

  4. Arm V8 - ADRP指令

    ADRP指令 作用 将当前指令所在页的基地址加/减去字节差,并写入目标寄存器 字节差:与目标地址页基地址的间隔字节数,其为PAGE_SIZE的整数倍 此时的字节差就是指令所操作的立即数 该指令通常配合 ...

  5. 机器人操作系统ROS (学习视频)—— 学习ROS,安装Ubuntu,虚拟机和双系统如何选择

    分享一个ROS入门视频: https://www.bilibili.com/video/BV1BP4y1o7pw/

  6. Python Pillow(PIL 第三方模块)和 cv2 (opencv第三方模块)对图片的 resize 操作 (缩放图片大小)

    PIL 模块的 resize  操作: 1.  从文件中读取图片,然后  resize  大小: import matplotlib.pyplot as plt import numpy as np ...

  7. 强化学习:AC算法中为什么不使用Q函数来表示优势函数

    强化学习中的策略梯度法(PG)不直接使用Q函数作为值函数来进行计算已经在Sutton的PG公式证明中提出,主要作用就是减少方差,因此使用优势函数进行计算.作为PG算法类中最常见的AC类算法有着较多的使 ...

  8. AI大模型 —— 国产大模型 —— 华为大模型

    有这么一句话,那就是AI大模型分两种,一种是大模型:另一种是华为大模型. 如果从技术角度来分析,华为的技术不论是在软件还是硬件都比国外的大公司差距极大,甚至有些技术评论者认为华为的软硬件技术至少落后2 ...

  9. 图片热区。vue3+ts和vue3+js写法(js没写完数据,功能完善)

    废话不多说,上代码 vue3+ts <!-- 热区组件 --> <template> <el-dialog v-model="dialog_visible&qu ...

  10. 2022年第一天,体验了一把wan(皖)式服务

    在新的一年到来之际, 一口君首先祝各位粉丝朋友新年快乐! 心想事成! 事业顺利! 阖家欢乐! 财源广进! 2022年的第一天,一口君带着家人去了躺马鞍山,享受了一下马鞍山的洗浴. 马鞍山桑拿虽然比不上 ...