# 权衡的艺术
“框架设计里到处都体现了权衡的艺术。”
# 命令式和声明式
# 命令式(更加关注过程)
个人理解为类似面向过程的思路,代码本身描述的是“做事的过程”。
// - 获取 id 为 app 的 div 标签
// - 它的文本内容为 hello world
// - 为其绑定点击事件
// - 当点击时弹出提示:ok
$('#app') // 获取 div
.text('hello world') // 设置文本内容
.on('click', () => {
alert('ok')
}) // 绑定点击事件
// 或者
const div = document.querySelector('#app') // 获取 div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => {
alert('ok')
}) // 绑定点击事件
# 声明式(更加关注结果)
个人理解为类似面向对象的思路,更加关注结果。
<!-- - 和上述逻辑类似 -->
<div @click="() => alert('ok')">hello world</div>
上述代码Vue.js 帮我们封装了过程(命令式),用户只需要关注结果(声明式)。
# 性能与可维护性的权衡
声明式代码的性能 不优于命令式代码的性能。
// 命令式代码 直接更新文本内容 性能最优
div.textContent = 'hello vue3' // 直接修改
对于框架来说,为了实现最优的更新性能,它需要找到前后的差。异并只更新变化的地方,但是最终完成这次更新的代码仍然是命令式的。
如果我们把直接修改的性能消耗定义为 A,把找出差异的性能消耗定义为 B,那么有:
命令式代码的更新性能消耗 = A
声明式代码的更新性能消耗 = B + A
声明式代码会比命令式代码多出找出差异的性能消 耗,因此最理想的情况是,当找出差异的性能消耗为 0 时,声明式代 码与命令式代码的性能相同,但是无法做到超越
框架本身就是 封装了命令式代码才实现了面向用户的声明式。
既然在性能层面命令式代码是更好的选择,那么为什么 Vue.js 要选择声明式的设计方案呢?
声明式代码的可维护性更强
命令式需要手动完成 DOM 元素的创建、更新、删除等工作。声明式代码不需要我们关心,
Vue.js
已经封装好了。
框架设计者要做的就是:在保持可维护性的同时让性能损失最 小化
# 虚拟 DOM 的性能
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
虚拟 DOM,就是为了最小化找出差异这一步的性能消耗而出现的
采用虚拟 DOM 的更新技术的性能理论上不可能比原生 JavaScript 操作 DOM 更高。
在大部分情况下,我们很难写出绝对优化的命令式代码,尤其是当应用程序的规模很大的时候。
# innerHTML
操作页面和虚拟 DOM
的性能对比。
// innerHTML 创建页面
const html = '<div id="app">hello world</div>'
document.body.innerHTML = html
innerHTML
创建页面的性能:HTML 字符串拼接的计算量 + innerHTML 的 DOM计算量。虚拟
DOM
创建页面的性能:创建 JavaScript 对象的 计算量 + 创建真实 DOM 的计算量。
由此得出在同一个数量级,则认为没有差异。在创建页面的时候,都需要新建所有 DOM 元素。虚拟DOM 相比 innerHTML 没有优势可言。
虚拟 DOM 和 innerHTML 在更新页面时的性能
innerHTML | 虚拟 DOM | |
---|---|---|
纯JS操作DOM | 渲染HTML字符串 | 创建新的虚拟DOM对象+diff算法计算差异 |
DOM运算 | 1. 销毁所有旧的DOM 2. 新建所有新的DOM | 必要时仅更新变化的DOM |
性能因素 | 与模版大小有关 | 与数据变换量有关 |
所以,在更新页面的时候,虚拟 DOM 相比 innerHTML 有着更好的性能表现。多一个diff算法的计算量,只更新变化的DOM。
innerHTML、虚拟 DOM 以及原生 JavaScript(指 createElement 等方法)在更新页面时的性能
性能差 | 性能中等 | 性能高 |
---|---|---|
心智负担中等 性能差 | 心智负担小 可维护性强 性能中等 | 心智负担大 可维护性差 性能高 |
# 运行时和编译时
框架设计一般分为 纯运行时的、运行时 + 编译时的、纯编译时的
Vue.js 3 是一个编译时 + 运行时的框架,它在保持灵活性的基础上,还能够通过编译手段分析用户提供的内容,从而进一步提升更新性能。
# 纯运行时的框架
提供一个 Render 函数,用户可以为该函数提供一个树型结构的数据对象,然后 Render 函数会根据该对象递归地将数据渲染成 DOM 元素
const obj = {
tag: 'div',
children: [{
tag: 'span',
children: 'hello world'
}]
}
每个对象都有两个属性:tag 代表标签名称,children 既可以是一个数组(代表子节点),也可以直接是一段文本(代表文本子节点)。接着,我们来实现 Render 函数:
function Render(obj, root) {
const el = document.createElement(obj.tag)
if (typeof obj.children === 'string') {
const text = document.createTextNode(obj.children)
el.appendChild(text)
} else if (obj.children) {
// 数组,递归调用 Render,使用 el 作为 root 参数
obj.children.forEach((child) => Render(child, el))
}
// 将元素添加到 root
root.appendChild(el)
const obj = {
tag: 'div',
children: [{
tag: 'span',
children: 'hello world'
}]
}
}
// 渲染到 body 下
Render(obj, document.body)
用户在使用它渲染内容时,直接为 Render 函数提供了一个树型结构的数据对象。手写树型结构的数据对象太麻烦了,而且不直观
# 运行时 + 编译时的框架
将HTML模板编译成 Render 函数需要的树型结构的数据对象。
const template = `
<div>
<span>hello world</span>
</div>
`
// 调用 Compiler 编译得到树型结构的数据对象
const obj = Compiler(html)
// 再调用 Render 进行渲染
Render(obj, document.body)
变成了一个运 行时 + 编译时的框架。它既支持运行时,用户可以直接提供数据对象从而无须编译;又支持编译时,用户可以提供 HTML 字符串,我们将其编译为数据对象后再交给运行时处理。
# 纯编译时的框架
把HTML 字符串编译成数据对象,直接编译成命令式代码。
const template = `
<div>
<span>hello world</span>
</div>
`
const div = docuemnt.createElement('div')
const span = docuemnt.createElement('span')
span.textContent = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
纯编译时的框架,因为我们不支持任何运行时内容,用户的代码通过编译器编译后才能运行