《Vue.js 设计与实现》读书笔记 - 第9章、简单 Diff 算法
第9章、简单 Diff 算法
9.1 减少 DOM 操作的性能开销
在之前的章节,如果新旧子节点的类型都是数组,我们会先卸载所有旧节点,再挂载所有新的子节点。但是如果存在相同类型的节点,我们完全可以复用节点,只修改类型即可。
所以这一节采取就朴素的复用思路,按顺序依次 patch 节点,如果旧子节点多就卸载,新子节点多就挂载。
for (let i = 0; i < minLen; i++) {
patch(oldChildren[i], newChildren[i], container)
}
9.2 DOM 复用与 key 的作用
因为直接按顺序复用并不总是合理,比如我把第一个节点移到最后,则所有的节点顺序都乱了。在 Vue 中数组节点都需要设置 key,所以我们可以通过 key 来找到复用的节点。
通过两层循环我们可以找到一个新节点对应的旧节点,然后把两个节点进行 patch。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
break
}
}
}
}
但是 patch 只是原地修改节点的属性,之后还需要移动节点。
9.3 找到需要移动的元素
判断移动的方式也很简单,遍历新子元素数组,找到对应的旧节点的下标并记录。如果后续的节点对应旧节点下标大于当前下标,则不需要移动,同时保留最大下标,否则表示需要移动。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
} else {
// 不需要移动 更新最大下标
lastIndex = j
}
break
}
}
}
}
9.4 如何移动元素
直接判断需要移动就移动到上一个(新)子元素后就可以了。
function patchChildren() {
// ... 上面都是之前的逻辑
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
}
}
9.5 添加新元素
感觉这种情况也非常好理解,无非是旧节点中没有对应的,那就在新节点对应的前一个节点后面加入就好了。
function patchChildren() {
// 只写 diff 相关逻辑
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let find = false
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
if (!find) {
// 没有对应的旧节点
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
}
新节点挂载需要使用 patch 来处理。不过之前的 patch 不能指定锚点,需要调整逻辑。
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
// ...
if (typeof type === 'string') {
if (!n1) {
// 挂载 传入anchor
mountElement(n2, container, anchor)
} else {
// 打补丁
patchElement(n1, n2)
}
}
// ...
}
function mountElement(vnode, container, anchor) {
// ...
insert(el, container, anchor)
}
9.6 移除不存在的元素
移除就是在旧节点找和新节点没有对应 key 的节点,然后卸载。
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
if (!has) {
unmount(oldVNode)
}
}
最后附本章完整代码:
点击查看代码
const { effect, ref } = VueReactivity
const Text = Symbol()
const Comment = Symbol()
const Fragment = Symbol()
function createRenderer(options) {
const {
createElement,
insert,
setElementText,
patchProps,
createText,
setText,
} = options
function mountElement(vnode, container, anchor) {
const el = (vnode.el = createElement(vnode.type))
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el)
})
}
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container, anchor)
}
// n1 旧node n2 新node
function patchElement(n1, n2) {
const el = (n2.el = n1.el)
const oldProps = n1.props
const newProps = n2.props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
// 更新children
patchChildren(n1, n2, el)
}
function patchChildren(n1, n2, container) {
// 如果新节点是字符串类型
if (typeof n2.children === 'string') {
// 新节点只有在为一组节点的时候需要卸载处理 其他情况不需要任何操作
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
}
// 设置新内容
setElementText(container, n2.children)
} else if (Array.isArray(n2.children)) {
// 如果新子元素是一组节点
if (Array.isArray(n1.children)) {
// 如果旧子节点也是一组节点
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let find = false
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
const ancher = prevVNode.el.nextSibling
// 把 newVNode.el 插入到前一个节点(prevVNode.el.nextSibling)的下一个节点之前
insert(newVNode.el, container, ancher)
} else {
// 不需要移动
lastIndex = j
}
break
}
}
if (!find) {
// 没有对应的就节点
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
const has = newChildren.find((vnode) => vnode.key === oldVNode.key)
if (!has) {
unmount(oldVNode)
}
}
} else {
// 否则旧节点不存在或者是字符串 只需要清空容器然后添加新节点就可以
setElementText(container, '')
n2.children.forEach((c) => patch(null, c, container))
}
} else {
// 新子节点不存在
if (Array.isArray(n1.children)) {
n1.children.forEach((c) => unmount(c))
} else if (typeof n1.children === 'string') {
setElementText(container, '')
}
}
}
// n1 旧node n2 新node container 容器
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
// 挂载 传入anchor
mountElement(n2, container, anchor)
} else {
// 打补丁
patchElement(n1, n2)
}
} else if (type === Text) {
if (!n1) {
const el = (n2.el = createText(n2.children))
insert(el, container)
} else {
const el = (n2.el = n1.el)
if (n2.children !== n1.children) {
// 更新文本节点内容
setText(el, n2.children)
}
}
} else if (type === Fragment) {
if (!n1) {
// 如果之前不存在 需要把节点一次挂载
n2.children.forEach((c) => patch(null, c, container))
} else {
// 之前存在只需要更新子节点即可
patchChildren(n1, n2, children)
}
} else if (typeof type === 'object') {
// 组件
} else if (type == 'xxx') {
// 处理其他类型的vnode
}
}
// 传入一个 vnode 卸载与其相关联的 DOM 节点
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
function render(vnode, container) {
console.log(vnode, container)
if (vnode) {
// 如果有新 vnode 就和旧 vnode 一起用 patch 处理
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 没有新 vnode 但是有旧 vnode 卸载
unmount(container._vnode)
}
}
// 把旧 vnode 缓存到 container
container._vnode = vnode
}
function hydrate(vnode, container) {
// 服务端渲染
}
return {
render,
hydrate,
}
}
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === 'form' && el.tagName === 'INPUT') return false
return key in el
}
const renderer = createRenderer({
createElement(tag) {
return document.createElement(tag)
},
setElementText(el, text) {
el.textContent = text
},
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
},
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
},
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// evl: vue event invoker
const invokers = el._vel || (el._vel = {})
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
invoker = el._vel[key] = (e) => {
// e.timeStamp 是事件发生的时间
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
// 存储事件被绑定的时间
invoker.attached = performance.now()
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
}
if (key === 'class') {
el.className = nextValue || ''
} else if (shouldSetAsProps(el, key, nextValue)) {
// 判断 key 是否存在对应的 DOM Properties
const type = typeof el[key] // 获取该属性的类型
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 没有对应的 DOM Properties
el.setAttribute(key, nextValue)
}
},
})
const vnode1 = {
type: 'div',
children: [
{
type: 'p',
children: '1',
key: 1,
},
{
type: 'div',
children: '2',
key: 2,
},
{
type: 'h1',
children: '3',
key: 3,
},
],
}
const vnode2 = {
type: 'div',
children: [
{
type: 'h1',
children: '3',
// key: 3,
},
{
type: 'p',
children: '1',
// key: 1,
},
{
type: 'div',
children: '4',
},
],
}
renderer.render(vnode1, document.querySelector('#app'))
renderer.render(vnode2, document.querySelector('#app'))
《Vue.js 设计与实现》读书笔记 - 第9章、简单 Diff 算法的更多相关文章
- 【vue.js权威指南】读书笔记(第一章)
最近在读新书<vue.js权威指南>,一边读,一边把笔记整理下来,方便自己以后温故知新,也希望能把自己的读书心得分享给大家. [第1章:遇见vue.js] vue.js是什么? vue.j ...
- 【vue.js权威指南】读书笔记(第二章)
[第2章:数据绑定] 何为数据绑定?答曰:数据绑定就是将数据和视图相关联,当数据发生变化的时候,可以自动的来更新视图. 数据绑定的语法主要分为以下几个部分: 文本插值:文本插值可以说是最基本的形式了. ...
- 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/ ...
- 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度
<Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...
- 《Linux内核设计与分析》第六周读书笔记——第三章
<Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...
- 《LINUX内核设计与实现》第三周读书笔记——第一二章
<Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...
- 《Linux内核设计与实现》第四周读书笔记——第五章
<Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...
- 《Linux内核设计与实现》第五周读书笔记——第十一章
<Linux内核设计与实现>第五周读书笔记——第十一章 20135301张忻 估算学习时间:共2.5小时 读书:2.0 代码:0 作业:0 博客:0.5 实际学习时间:共3.0小时 读书: ...
- 《Linux内核设计与实现》读书笔记——第五章
<Linux内核设计与实现>读书笔记--第五章 标签(空格分隔): 20135321余佳源 第五章 系统调用 操作系统中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限 ...
随机推荐
- P10245 Swimming Pool题解
P10245 Swimming Pool 题意 给你四条边 \(abcd\),求这四条边是否可以组成梯形. 思路 这显然是一道简单的普通数学题. 判断是否能构成梯形只需看四条边是否能满足,上底减下底的 ...
- python selenium元素定位
1.ID元素定位基于元素属性中的id的值来进行定位,id是一个标签的唯一属性值可以通过id属性来唯一定位一个元素,是首选的元素定位方式,动态ID不做考虑.driver .find_element_by ...
- 【SpringBoot】06 探索配置方式 Part2 占位符的使用
配置占位符? 随机数配置生成 RandomValuePropertySource 在配置文件中使用随机数 uid = ${random.value} uid = ${random.int} uid = ...
- 【Dos-BatchPrograming】04
--1.PING 主机联通性检测 Microsoft Windows [版本 10.0.19041.746] (c) 2020 Microsoft Corporation. 保留所有权利. C:\Us ...
- 【Shiro】05 自定义Realm认证实现
[前提情要] Shiro默认使用自带的IniRealm,IniRealm从ini配置文件中读取用户的信息, 大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm. 根接口:Realm ...
- 【MySQL】29 索引
MySQL是一个关系型的数据库 使用标准的SQL数据格语言格式 支持大型数据库,处理千万级别的记录数据 允许多系统运行,支持多种编程语言连接 最重要的一点是MySQL允许定制,采用GPL协议,允许修改 ...
- ComfyUI插件:ComfyUI layer style 节点(四)
前言: 学习ComfyUI是一场持久战,而ComfyUI layer style 是一组专为图片设计制作且集成了Photoshop功能的强大节点.该节点几乎将PhotoShop的全部功能迁移到Comf ...
- 贝塔分布 beta分布的累积分布函数(CDF)计算 —— 如何使用二项式分布表示beta分布的概率累积函数
贝塔分布 beta分布的累积分布函数(CDF)的计算公式: 计算beta分布的累积分布函数(CDF)是需要计算积分的,但是最近发现另一种计算方法,即,使用二项式分布计算beta分布的概率累积函数. b ...
- Apache SeaTunnel 2.3.3 版本发布,CDC 支持 Schema Evolution!
时隔两个月, Apache SeaTunnel 终于迎来大版本更新.此次发布的 2.3.3 版本在功能和性能上均有较大优化改进,其中大家期待已久的 CDC Schema evolution(DDL 变 ...
- ComfyUI插件:ComfyUI_Noise节点
前言: 学习ComfyUI是一场持久战,ComfyUI_Noise是对ComfyUI中的噪声进行控制的一个插件库,该库可以完成图像噪声的反推,并通过采样再以几乎无损的方式返回原图,通过该库的使用可以更 ...