《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余佳源 第五章 系统调用 操作系统中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限 ...
随机推荐
- DuiLib的编译
Duilib编译需要注意两点: 加入预处理器:WIN32;_DEBUG;_WINDOWS;UILIB_STATIC; 到这一步还是报错,报错的是DuiString += 这一行报错,还有Util这 ...
- git操作之一:git add/commit/init
在日常的开发中,适用版本控制系统来进行协同开发已经是工作中的常态,使用比较多的要数git这个工具,今天就来说下git的日常用法以及在开发中的一些疑惑. 一.概述 git在日常开发中广泛应用,其概念可以 ...
- 这本vue3编译原理开源电子书,初中级前端竟然都能看懂
前言 众所周知vue提供了很多黑魔法,比如单文件组件(SFC).指令.宏函数.css scoped等.这些都是vue提供的开箱即用的功能,大家平时用这些黑魔法的时候有没有疑惑过一些疑问呢. 我们每天写 ...
- telegraf 常用命令总结
本文为博主原创,转载请注明出处: Telegraf 是一个灵活的服务器代理,用于收集和报告指标.它支持插件驱动,这意味着你可以根据需要添加或修改功能. 1.使用telegraf --help 查看te ...
- 【SQL】 牛客网SQL训练Part2 中等难度
查找当前薪水详情以及部门编号dept_no 查找 1.各个部门当前领导的薪水详情以及其对应部门编号dept_no, 2.输出结果以salaries.emp_no升序排序, 3.并且请注意输出结果里面d ...
- 【WEB】URL文件
早些年接触电脑的时候就有这个东西,去网站上下载盗版游戏,网站会附加这种URL文件 双击运行之后是打开浏览器跳转到该文件描述的网址 我从来没想过这东西里面写的是什么 百度经验: https://baij ...
- 【DataBase】XueSQL Training
地址: http://xuesql.cn/ Lesson0 -- 认识SQL -- [初体验]这是第一题,请你先将左侧的输入框里的内容清空,然后请输入下面的SQL,您将看到所有电影标题: SELECT ...
- 为什么要使用工业仿真软件? —— CAE(Computer Aided Engineering)工程设计中的计算机辅助工程
CAE技术: 引自: https://baike.baidu.com/item/CAE技术/18884456?fr=ge_ala 引自: https://www.mscsoftware.com.cn/ ...
- AI开源是否应该完全开源?AI的完全开源是否可以实现?
看了一个视频: 袁进辉:零代码改动,加速AIGC 里面提到了一个完全开源的概念,感觉有些意思,虽然觉得可实现性不高,嘿嘿嘿!!! AI的完全开源: 训练数据开源.数据清洗过程开源.模型权重开源.项目代 ...
- 【转载】 ReLu(Rectified Linear Units)激活函数
原文地址: https://www.cnblogs.com/neopenx/p/4453161.html ============================== 论文参考:Deep Sparse ...