学习vue3之前需要知道各个模块的实现思路和细节。
1. 框架设计成命令式还是声明式? 两者区别和优缺点呢?
2. 框架要设计成纯运行时还是纯编译时?或者运行时+纯编译时?三者区别和优缺点呢?
1.1 命令式和声明式
jQuery是命令式框架,特点是关注过程。
1. 获取id为app的div标签
2. 它的文本内容为hello world
3. 为其绑定点击事件
4. 当点击时弹出提示:ok
对应的代码为:
$("#app") // 获取div
.text('hello world') //设置文本内容
.on('click', () => { alert('ok') }) // 绑定点击事件
可以看出,自然描述语言能与代码产生一一对应的关系,代码本身描述的是“做事的过程”。
vue是声明式框架,特点是关注结果
上面的例子,通过vue的话,实现如下:
<div @click="() => alert('ok')">hello world</div>
也就是说,vue帮我们封装了过程, 内部实现是命令式的,暴露给用户的更加声明式
1.2 性能与可维护性的权衡
声明式代码的性能不优于命令式代码的性能
例如,修改div文本内容为hello vue3
命令式代码实现:
div.textContent = 'hello vue3'
声明式代码实现:
先看实现前后差异:
// 修改之前
div.textContent = 'hello world'
// 修改之后
div.textContent = 'hello vue3'
那么声明式代码只关注结果,vue实现是找到差异+修改内容才可
如果定义修改性能消耗为 A, 查找消耗为 B
命令式代码性能消耗: A
声明式代码代码性能消耗: A + B
所以 声明式代码代码性能消耗<=命令式代码性能消耗
声明式代码性能不一定优,那为何vue会选择声明式呢?
为了更好的代码可维护性以及代码的直观体现!不必再去纠结于元素的增删改查。
在保持可维护性的同时让性能损失最小化, 即性能与可维护性的权衡
1.3 虚拟DOM的性能到底如何
为什么会使用虚拟DOM
由上一节得出:
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
那么如果能最小化找出差异的性能消耗, 就可以使得声明式代码性能无限接近命令式代码的性能,所谓的虚拟DOM, 就是为了解决这一问题而出现的。
创建页面时性能对比
innerHTML和虚拟DOM在创建页面时的性能对比图:
| 虚拟DOM | innerHTML | |
|---|---|---|
| 纯JavaSript运算 | 创建JavaScript对象(VNode) | 渲染HTML字符串 |
| DOM运算 | 新建所有DOM元素 | 新建所有DOM元素 |
注:在创建页面时都需要新建所有DOM元素,所以只需要在宏观的角度只看数量级上的差异
更新页面时性能对比
innerHTML和虚拟DOM在更新页面时的性能对比图:
| 虚拟DOM | innerHTML | |
|---|---|---|
| 纯JavaSript运算 | 创建JavaScript对象 + Diff | 渲染HTML字符串 |
| DOM运算 | 必要的DOM更新 | 销毁所有旧DOM 新建所有新DOM |
| 性能因素 | 与数据变化量相关 | 与模板大小相关 |
注:更新页面时,在纯JavaSript运算层面虚拟DOM会多处一个Diff的性能销毁,但是也构不成数量级的差异,而在DOM运算层面,虚拟DOM只进行必要的更新,这时候虚拟DOM的优势就体现出来了
基于此,可以总结innerHTML、虚拟DOM以及原生JavaScript(指createElement等方法)更新页面时的性能,如图所示:
innerHTML(模板)< 虚拟DOM < 原生JavaScript
| innerHTML(模板) | 虚拟DOM | 原生JavaScript |
|---|---|---|
| 心智负担中等 | 心智负担小 | 心智负担大 |
| 可维护性强 | 可维护性差 | |
| 性能差 | 性能不错 | 性能差 |
有没有办法做到,既声明式地描述UI,又具备原生JavaScript的性能呢?
1.4 运行时和编译时
设计框架时,一般有三种选择:纯运行时的、运行时+编译时的或纯编译时的
纯运行时的
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world'}
]
}
function Render(obj, root) {
const el = document.createElement(obj.tag)
// obj.children为字符串直接添加进el
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)
}
// 渲染到body下
Render(obj, document.body)
运行结果如下:
特点:需要手写树形结构的对象,不直观
运行时+编译时的
假设有个编译函数Compiler,可以将模板字符串转化为树型结构的数据对象
const html = `
<div>
<span>hello world</span>
</div>
// 调用 Compiler编译得到树型结构的数据对象
const obj = Complier(htnl)
// 再调用 Render进行渲染
Render(obj, document.body)
纯编译时的
const div = document.createElement('div')
const span = document.createElement('span')
span.innerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
对比分析
- 纯运行时的:没有编译的过程,没法分析用户提供的内容未来会不会改变
- 运行时+编译时的:有编译过程,可以分析内容,可以为Render提供优化空间
- 纯编译时的:也可以分析用户提供的内容,不经过其他运行过程,直接编译可能性能更好,但是有损灵活性
Vue3 保持了运行时编译时的架构,在保持灵活性的基础上尽可能的优化,甚至不输纯编译时的框架
总结
- 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
- 虚拟DOM的意义在于使找出差异的性能消耗最小化
- innerHTML、虚拟DOM以及原生JavaScript(指createElement等方法)三者操作DOM的性能,不可以简单的下定论,这与
页面大小、变更部分的大小都有关系,还需要结合心智负担、可维护性等因素综合考虑,发现虚拟DOM是个还不错的选择 - Vue3是一个运行时+编译时的框架,它在保持灵活性的基础上,还能通过编译手段分析用户提供的内容,从而进一步提升更新性能