《Vue.js 设计与实现》读书笔记 - 第8章、挂载与更新
第8章、挂载与更新
8.1 挂载子节点和元素的属性
扩展子元素的类型可以为数组,并判断如果是数组的话,就先依次挂载所有的子元素。
同时新增节点属性。属性可以通过 el.setAttribute
添加到 DOM 上,也可以直接在 DOM 对象上设置。
function createRenderer(options) {
const { createElement, insert, setElementText } = options
function mountElement(vnode, container) {
const 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) {
el.setAttribute(key, vnode.props[key])
// el[key] = vnode.props[key]
}
}
insert(el, container)
}
// ... 同上一章 省略
}
const renderer = createRenderer({
// ...
})
renderer.render(
{
type: 'div',
props: {
id: 'foo'
},
children: [
{
type: 'p',
children: 'hello'
}
],
},
document.getElementById('app')
)
8.2 HTML Attributes 与 DOM Properties
HTML Attributes:HTML 标签上的属性,比如
<div id="foo"></div>
这里的id
DOM Properties:JavaScript 中 DOM 对象的属性。如下图,DOM 对象上的属性:
HTML Attributes 和 DOM Properties 的名字不总是相同,并不是所有的 HTML Attributes 都有对应的 DOM Properties,也不是所有的 DOM Properties 都有对应的 HTML Attributes
- HTML Attributes 中的
class
在 DOM Properties 为className
- HTML Attributes 中
<div aria-valuenow="75"></div>
中的aria-*
类在 DOM Properties 没有对应值。 - DOM Properties 中的
el.textContent
在 HTML Attributes 也没有对应属性。
- HTML Attributes 中的
HTML Attributes 和 DOM Properties 具体相同名称的看作 直接映射。
HTML Attributes 的作用是设置与之对应的 DOM Properties 的初始值。(
input
的value
和defaultValue
)
8.3 正确的设置元素属性
在 <button disabled>Button</button>
中,对应的 vnode 为
{
type: 'node',
props: {
disabled: ''
}
}
是在表示禁用。而 <button disabled="false">Button</button>
中,对应的 vnode 为 {disabled: false}
是在表示不禁用。
如果设置 HTML Attributes 的话 el.setAttribute(key, 'false')
会被设置为禁用,设置 DOM Properties 的话 el.disabled = ''
又会把 ''
转为 false
设置成不禁用。我们我们需要对 Boolean 值进行特殊处理。
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// ...
if (vnode.props) {
for (const key in vnode.props) {
if (key in el) {
// 判断 key 是否存在对应的 DOM Properties
const type = typeof el[key] // 获取该属性的类型
const value = vnode.props[key]
if (type === 'boolean' && value === '') {
el[key] = true
} else {
el[key] = value
}
} else {
// 没有对应的 DOM Properties
el.setAttribute(key, vnode.props[key])
}
}
}
insert(el, container)
}
还有种特殊情况,在 input
中 form
对应的 DOM Properties 属性是只读的。我们需要先判断是否只读再决定如何修改属性。所以新增函数,用于判断一个属性是否要通过 DOM Properties 来设置。
function shouldSetAsProps(el, key, value) {
// 特殊处理(可能有很多需要特殊处理的情况 此处只列这一种)
if (key === 'form' && el.tagName === 'INPUT') return false
return key in el
}
接下来要把上面的代码中平台相关的部分抽出来。
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// ...
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container)
}
const renderer = createRenderer({
// ...
patchProps(el, key, prevValue, nextValue) {
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)
}
},
})
8.4 class 的处理
在 Vue 中,可以通过字符串,对象,数组的方式来设置类名,所以我们需要对各种方式的书写做处理,处理为统一的字符串格式。这里假设只有字符串形式。
同时在浏览器中使用 el.className
设置类名的效率是最高的,所以这里特殊处理一下使用该方法类设置域名。
patchProps(el, key, prevValue, nextValue) {
if (key === 'class') {
el.className = nextValue || ''
} else if (shouldSetAsProps(el, key, nextValue)) {
//...
},
- 为用户提供便利的代价是在底层做统一化的处理,这消耗了更多的性能。(
style
也是) vnode.props
不是总和 DOM 元素属性保持一致,这取决于上层 API 设计。
8.5 卸载操作
前文通过 container.innerHTML = ''
来清空容器,这样做并不严谨。原因如下:
- 需要调用相关生命周期函数,如
beforeUnmount
- 需要自定自定义指令的卸载钩子函数
- 需要移除绑定的事件函数
我们需要找到关联的 DOM 元素,并使用原生 DOM 操作方法将该 DOM 元素移除。因此,我们需要给 vnode 绑定对应的 DOM 元素。
function mountElement(vnode, container) {
const el = (vnode.el = createElement(vnode.type))
// ...
}
然后修改 render
中的卸载逻辑:
// 传入一个 vnode 卸载与其相关联的 DOM 节点
function unmount(vnode) {
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
function render(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
}
8.6 区分 vnode 的类型
当我们传递了新旧两个节点,来使用 patch
打补丁的时候,我们需要在 patch
判断新旧节点的类型,如果类型不同那就先卸载再挂载,节点类型相同时才有打补丁的意义。
// n1 旧node n2 新node container 容器
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
if (!n1) {
// 挂载
mountElement(n2, container)
} else {
// 打补丁
patchElement(n1, n2)
}
} else if (typeof type === 'object') {
// 组件
} else if (type == 'xxx') {
// 处理其他类型的vnode
}
}
可以看到 patch
的具体操作还是和 vnode 的类型有关,要看是原始的 HTML 类型还是组件或其他。
8.7 事件的处理
在 vnode 中,我们约定把 on
开头的属性视作事件。然后我们可以通过 addEventListener
函数来绑定事件。
如果之前就有值,我们会自然想到先移除旧事件再绑定新事件,不过还有更优雅的方式,就是我们存储一个事件处理函数,并把真正的事件函数赋值到该函数。
我真的觉得这个处理方式好牛逼啊!!!!!
patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
// evl: vue event invoker
let invoker = el._vel
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
invoker = el._vel = (e) => {
invoker.value(e)
}
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
}
// ... 省略之前的代码
},
// 使用
renderer.render(
{
type: 'button',
props: {
onClick: () => {
console.log('click!')
},
},
children: 'Button',
},
document.getElementById('app')
)
由于上面把所有的时间都通过 invoker
存储,如果有多种事件的话会相互覆盖,所以应该把 invoker
设计为一个对象。同时,同一个事件也可能绑定多个事件函数,我们还需要判断是否为数组。
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) => {
if (Array.isArray(invoker.value)) {
invoker.value.forEach((fn) => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
invoker.value = nextValue
}
} else if (invoker) {
el.removeEventListener(name, invoker)
}
}
// ...
},
8.8 事件冒泡与更新时机问题
看如下示例(patchElement
函数的实现在 8.9 但是下面的示例需要用到。。。),我们定义了两个节点,父节点一开始点击事件为空,点击子节点会切换父元素的点击事件。
const bol = ref(false)
effect(() => {
const vnode = {
type: 'div',
props: bol.value
? {
onClick: () => {
alert('父元素 clicked')
},
}
: {},
children: [
{
type: 'p',
props: {
onClick: () => {
bol.value = true
},
},
children: 'text',
},
],
}
renderer.render(vnode, document.querySelector('#app'))
})
现在我们点击子节点会发现父元素的事件被执行了。原因是子元素点击后副作用函数会被重新执行,我们先执行副作用函数把父元素事件调整了之后,冒泡才到父元素的 DOM 节点,导致事件函数被执行。
解决方法:记录事件触发的时间和事件绑定的时间,只有触发时间在绑定时间之后才会执行。
但这里书中的处理方法我认为有问题,不确定,给老师提了意见,再看看。
8.9 更新子节点
元素的子节点分为三种类型
- 没有子节点
vnode.children = null
- 文本子节点
typeof vnode.children = string
- 其他情况,单个元素或多个子节点,此时用数组表示
我们在新旧子节点切换的时候,理论上是这三种的互相切换,就是有 9 种可能。现在实现之前没有实现的 patchElement
函数(就是之前新旧节点都存在时,patch
中用于处理的函数)。
// 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)) {
// 如果旧子节点也是一组节点 后续使用核心的diff算法
// 暂时先全部卸载再重新添加
n1.children.forEach((c) => unmount(c))
n2.children.forEach((c) => patch(null, c, container))
} 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, '')
}
}
}
在 patchChildren
分别对 9 种情况讨论处理。其中 diff 会在后面章节实现。
8.10 文本节点和注释节点
我们使用 type: 'div'
这种形式表示一个 HTML 中的普通标签,但是对于注释节点和文本节点是没有标签的。所以我们使用 Symbol 来表示,如下:
const Text = Symbol()
const newVnode = {
type: Text,
children: '我是文本内容'
}
const Comment = Symbol()
const newVnode = {
type: Comment,
children: '我是注释内容'
}
调整之前的 patch
函数,新增文本类型的判断。
function createRenderer(options) {
const { createElement, insert, setElementText, patchProps, createText, setText } = options
// ...
// n1 旧node n2 新node container 容器
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// ...
} 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 (typeof type === 'object') {
// 组件
} else if (type == 'xxx') {
// 处理其他类型的vnode
}
}
// ...
}
const renderer = createRenderer({
// ...
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
},
patchProps(el, key, prevValue, nextValue) {
// ...
}
})
注释节点和文本节点逻辑类似,不过需要使用 document.createComment
来创建节点。
8.11 Fragment
在 Vue3 中允许多根节点模板,实际上是通过 Fragment 来实现的。Fragment 没有标签名,也通过 Symbol
作为唯一表标识。
const Fragment = Symbol()
// ...
// 在 patch 中 Fragment 的更新逻辑
else if (type === Fragment) {
if (!n1) {
// 如果之前不存在 需要把节点一次挂载
n2.children.forEach((c) => patch(null, c, container))
} else {
// 之前存在只需要更新子节点即可
patchChildren(n1, n2, children)
}
}
// unmount 中添加 Fragment 的逻辑,因为 Fragment 没有实际节点 只需要卸载子节点
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)
}
}
《Vue.js 设计与实现》读书笔记 - 第8章、挂载与更新的更多相关文章
- 【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余佳源 第五章 系统调用 操作系统中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限 ...
随机推荐
- 【服务器】Ubuntu虚拟内存设置
引子 最近服务器内存老是爆掉,64G的内存对于四五个人同时使用还是有点勉强,上网查询了一下虚拟内存的教程,本博客记录一下方法. swap内存设置 假设你想将swap文件放在/mnt/data/mem目 ...
- C#中使用 record 的好处 因为好用所以推荐~
一晃距C# 9发布已经4年了,对于record关键字想必大家都不陌生了,不过呢发现还是有很多同学不屑于使用这个语法糖,确实,本质上record就是class的封装,能用 record 书写的类,那10 ...
- WordPress基础之菜单导航栏设置
菜单是WordPress的一项重要功能,方便用户快速打开网站页面,我们通常说的网站导航栏就是菜单.菜单通常显示在网站的顶部或者底部,以Apple官网的为例: 这篇文章我们就学习下:如何添加.删除菜单: ...
- Java--普通方法重载
[转载自本科老师上课课件] 调用一个重载过的方法时,Java编译程序是如何确定究竟应该调用哪一个方法?以下代码定义了三个重载方法: public void f(char ch){ System.out ...
- 【Axure RP】Axure RP 9 下载安装及汉化
本体及破解机下载: http://www.sd173.com/soft/7951.html 汉化补丁教程见: https://blog.csdn.net/weixin_74457789/article ...
- 【Kotlin】官网学习笔记
0.IDEA环境设置: 1.基础语法 BasicSyntax 地址:https://kotlinlang.org/docs/basic-syntax.html 一.方法与变量 可以直接编写main方法 ...
- 关于英语的语言规范问题——美式英语、英式英语和中式英语(Chinese English)到底哪个才是正宗 —— 中式英语才是英语世界的未来
因为日常生成生活中总是会使用英语进行阅读.写作.学习和交流表达,由于小的时候是学传统正宗英语(英式英语),后来长大后因为美国实力强又开始学这个时候的正宗英语(美式英语),但是由于个人的能力问题(农村娃 ...
- 在国产超算平台上(aarch64架构)安装pytorch-cuda失败,究其原因竟是官方未提供对应的cuda版本——pip方式和conda方式均无法获得相应cuda版本
最近在国产超算平台上安装pytorch,但是怎么弄都会报错: raise AssertionError("Torch not compiled with CUDA enabled" ...
- baselines算法库run.py模块分析
baselines算法库地址: https://gitee.com/devilmaycry812839668/baselines =================================== ...
- vue&element项目实战 之api模块化与公共字典
4.api模块化配置 步骤一:编写字典api即dic.js import request from '@/utils/request' // 查询字典列表 export const getDicLis ...