《Vue.js 设计与实现》读书笔记 - 第11章、快速 Diff 算法
第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 算法的更多相关文章
- 【vue.js权威指南】读书笔记(第一章)
最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...
- 【vue.js权威指南】读书笔记(第二章)
[第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...
- C++ primer plus读书笔记——第11章 使用类
第11章 使用类 1. 运算符重载是一种形式的C++多态. 2. 不要返回指向局部变量或临时对象的引用.函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据. 3. 运算符重载的格式如下: ...
- 《Unix环境高级编程》读书笔记 第11章-线程
1. 引言 了解如何使用多个控制线程在单进程环境中执行多个任务. 不管在什么情况下,只要单个资源需要在多个用户键共享,就必须处理一致性问题. 2. 线程概念 典型的Unix进程可以看成只有一个控制线程 ...
- 《C和指针》 读书笔记 -- 第11章 动态内存分配
1.C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放,这些函数维护一个可用内存池. void *malloc(size_t size);//返回指向分配的内存块起始位置的 ...
- 《JavaScript Dom 编程艺术》读书笔记-第11章
本章简单介绍了HTML5,并推荐了一个好工具Modernizr,用于检测浏览器可能支持的各种特性. HTML5的新特性包括: 可以用来在文档中绘制矢量及位图的<canvas>元素: 可以在 ...
- INSPIRED启示录 读书笔记 - 第11章 评估产品机会
市场需求文档 大多数的公司产品选择权是由高管.市场部门.开发团队甚至是大客户,在这种情况下公司会跳过市场需求文档或是误写成产品规范文档,回避评估产品机会 在正常情况下,应该是由业务人员会撰写一份论证产 ...
- Java 核心读书笔记 第11章
1. 异常 用户希望在出现错误时,程序能够采用一些理智的行为. 如果由于出现错误而使得某些操作无法完成,程序应该: 返回到一种安全状态,并能够进行一些其他的命令: 或者:允许用于保存所有操作的结果, ...
- Linux内核设计与实现 读书笔记 转
Linux内核设计与实现 读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...
- 【2018.08.13 C与C++基础】C++语言的设计与演化读书笔记
先占坑 老实说看这本书的时候,有很多地方都很迷糊,但却说不清楚问题到底在哪里,只能和Effective C++联系起来,更深层次的东西就想不到了. 链接: https://blog.csdn.net/ ...
随机推荐
- C#开源、简单易用的Dapper扩展类库 - Dommel
前言 今天大姚给大家分享一个C#开源(MIT License).免费.简单易用的Dapper扩展类库,帮助.NET开发者使用Dapper的CRUD操作变得更简单:Dommel. 项目特性 Dommel ...
- 【Eclipse】入门使用
Eclipse界面简单概述 第一次启动时,工作空间的选择 工作界面的介绍: 选项条 工具栏 工程浏览窗口 工程大纲窗口 控制台输出窗口 在窗口选项中悬浮放在Show View选项中可以查看所有的窗口 ...
- 暑假自学Java进度总结04
一.今日所学: 1.下载并使用idea开发工具 1>了解idea的发展历史 2>尝试用idea编写代码 3>学习idea中的项目和模块操作 2.学习赋值运算符 加后赋值:" ...
- Arm V8 - ADRP指令
ADRP指令 作用 将当前指令所在页的基地址加/减去字节差,并写入目标寄存器 字节差:与目标地址页基地址的间隔字节数,其为PAGE_SIZE的整数倍 此时的字节差就是指令所操作的立即数 该指令通常配合 ...
- 机器人操作系统ROS (学习视频)—— 学习ROS,安装Ubuntu,虚拟机和双系统如何选择
分享一个ROS入门视频: https://www.bilibili.com/video/BV1BP4y1o7pw/
- Python Pillow(PIL 第三方模块)和 cv2 (opencv第三方模块)对图片的 resize 操作 (缩放图片大小)
PIL 模块的 resize 操作: 1. 从文件中读取图片,然后 resize 大小: import matplotlib.pyplot as plt import numpy as np ...
- 强化学习:AC算法中为什么不使用Q函数来表示优势函数
强化学习中的策略梯度法(PG)不直接使用Q函数作为值函数来进行计算已经在Sutton的PG公式证明中提出,主要作用就是减少方差,因此使用优势函数进行计算.作为PG算法类中最常见的AC类算法有着较多的使 ...
- AI大模型 —— 国产大模型 —— 华为大模型
有这么一句话,那就是AI大模型分两种,一种是大模型:另一种是华为大模型. 如果从技术角度来分析,华为的技术不论是在软件还是硬件都比国外的大公司差距极大,甚至有些技术评论者认为华为的软硬件技术至少落后2 ...
- 图片热区。vue3+ts和vue3+js写法(js没写完数据,功能完善)
废话不多说,上代码 vue3+ts <!-- 热区组件 --> <template> <el-dialog v-model="dialog_visible&qu ...
- 2022年第一天,体验了一把wan(皖)式服务
在新的一年到来之际, 一口君首先祝各位粉丝朋友新年快乐! 心想事成! 事业顺利! 阖家欢乐! 财源广进! 2022年的第一天,一口君带着家人去了躺马鞍山,享受了一下马鞍山的洗浴. 马鞍山桑拿虽然比不上 ...