第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 和 DOM Properties 具体相同名称的看作 直接映射。

  • HTML Attributes 的作用是设置与之对应的 DOM Properties 的初始值。(inputvaluedefaultValue

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)
}

还有种特殊情况,在 inputform 对应的 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 = '' 来清空容器,这样做并不严谨。原因如下:

  1. 需要调用相关生命周期函数,如 beforeUnmount
  2. 需要自定自定义指令的卸载钩子函数
  3. 需要移除绑定的事件函数

我们需要找到关联的 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章、挂载与更新的更多相关文章

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

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

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

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

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

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

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

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

  5. 《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

    <Linux内核设计与实现>第八周读书笔记——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配 ...

  6. 《Linux内核设计与分析》第六周读书笔记——第三章

    <Linux内核设计与实现>第六周读书笔记——第三章 20135301张忻估算学习时间:共2.5小时读书:2.0代码:0作业:0博客:0.5实际学习时间:共3.0小时读书:2.0代码:0作 ...

  7. 《LINUX内核设计与实现》第三周读书笔记——第一二章

    <Linux内核设计与实现>读书笔记--第一二章 20135301张忻 估算学习时间:共2小时 读书:1.5 代码:0 作业:0 博客:0.5 实际学习时间:共2.5小时 读书:2.0 代 ...

  8. 《Linux内核设计与实现》第四周读书笔记——第五章

    <Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...

  9. 《Linux内核设计与实现》第五周读书笔记——第十一章

    <Linux内核设计与实现>第五周读书笔记——第十一章 20135301张忻 估算学习时间:共2.5小时 读书:2.0 代码:0 作业:0 博客:0.5 实际学习时间:共3.0小时 读书: ...

  10. 《Linux内核设计与实现》读书笔记——第五章

    <Linux内核设计与实现>读书笔记--第五章 标签(空格分隔): 20135321余佳源 第五章 系统调用 操作系统中,内核提供了用户进程与内核进行交互的一组接口.这些接口让应用程序受限 ...

随机推荐

  1. 【服务器】Ubuntu虚拟内存设置

    引子 最近服务器内存老是爆掉,64G的内存对于四五个人同时使用还是有点勉强,上网查询了一下虚拟内存的教程,本博客记录一下方法. swap内存设置 假设你想将swap文件放在/mnt/data/mem目 ...

  2. C#中使用 record 的好处 因为好用所以推荐~

    一晃距C# 9发布已经4年了,对于record关键字想必大家都不陌生了,不过呢发现还是有很多同学不屑于使用这个语法糖,确实,本质上record就是class的封装,能用 record 书写的类,那10 ...

  3. WordPress基础之菜单导航栏设置

    菜单是WordPress的一项重要功能,方便用户快速打开网站页面,我们通常说的网站导航栏就是菜单.菜单通常显示在网站的顶部或者底部,以Apple官网的为例: 这篇文章我们就学习下:如何添加.删除菜单: ...

  4. Java--普通方法重载

    [转载自本科老师上课课件] 调用一个重载过的方法时,Java编译程序是如何确定究竟应该调用哪一个方法?以下代码定义了三个重载方法: public void f(char ch){ System.out ...

  5. 【Axure RP】Axure RP 9 下载安装及汉化

    本体及破解机下载: http://www.sd173.com/soft/7951.html 汉化补丁教程见: https://blog.csdn.net/weixin_74457789/article ...

  6. 【Kotlin】官网学习笔记

    0.IDEA环境设置: 1.基础语法 BasicSyntax 地址:https://kotlinlang.org/docs/basic-syntax.html 一.方法与变量 可以直接编写main方法 ...

  7. 关于英语的语言规范问题——美式英语、英式英语和中式英语(Chinese English)到底哪个才是正宗 —— 中式英语才是英语世界的未来

    因为日常生成生活中总是会使用英语进行阅读.写作.学习和交流表达,由于小的时候是学传统正宗英语(英式英语),后来长大后因为美国实力强又开始学这个时候的正宗英语(美式英语),但是由于个人的能力问题(农村娃 ...

  8. 在国产超算平台上(aarch64架构)安装pytorch-cuda失败,究其原因竟是官方未提供对应的cuda版本——pip方式和conda方式均无法获得相应cuda版本

    最近在国产超算平台上安装pytorch,但是怎么弄都会报错: raise AssertionError("Torch not compiled with CUDA enabled" ...

  9. baselines算法库run.py模块分析

    baselines算法库地址: https://gitee.com/devilmaycry812839668/baselines =================================== ...

  10. vue&element项目实战 之api模块化与公共字典

    4.api模块化配置 步骤一:编写字典api即dic.js import request from '@/utils/request' // 查询字典列表 export const getDicLis ...