持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情
1.1 声明式和命令式
框架从范式上被分为了两种: 声明式和命令式, 两种范式各有优缺点。
1.1.1 命令式框架
关注过程 是命令式框架的一大特点。即代码本身描述的是 做事的过程。 它使得自然语言能够和代码产生一一对应的关系。
比如下面这段话,我们使用JQuery翻译出来
- 获取ID为app的div标签;
- 将其文本内容设为:hello world;
- 为其绑定点击事件;
- 当点击时弹出提示:ok;
翻译为JQuery就是下面这样
$('#app) // 获取ID为app的元素
.text('hello world) // 设定文本内容
.on('click',function(){ // 绑定点击事件
alert('ok')
})
使用原生JavaScript翻译则是这样的
const app = document.getElementById('app') // 获取ID为app的元素
app.innerHTML = 'hell world' // 改变其文本内容
app.addEventListener('click',function(){
alert('ok')
})
从上面的这两个代码翻译可以看出,我们说的每一句话都能找到对应的代码,因此,命令式框架更符合我们的逻辑直觉。
1.1.2 声明式框架
声明式框架的关注点与命令式框架不同,它的侧重点是 关注结果。
同样是上面的那一段话,用声明式框架翻译则是下面这样
<div @click="()=> alert('ok')">hello world</div>
可以看到,这里提供的是一个结果,至于如何去实现这个结果,则是由声明式框架来帮我们完成,也就是说 Vue.js帮我们封装了过程。 因此,我们不难猜到,Vue的内部肯定是命令式的,但是它暴露给用户的却是更加声明式。
1.2 性能与可维护性的权衡
这一小节的核心点在于这一个结论: 声明式代码的性能不优于命令式代码的性能。
比如上面的代码中,我们要把元素的文字内容从hello world改为hello vue3,看看两种代码的处理方式:
// 命令式
app.textContent = 'hello vue3' // 直接修改
命令式的代码因为我们明确知道要修改什么,因此我们可以直接调用相关命令操作即可,就如上面那样。
而声明式代码则不一定能做到这样,因为它所描述的是结果。
<!-- 之前 -->
<div @click="()=> alert('ok')">hello world</div>
<!-- 之后 -->
<div @click="()=> alert('ok')">hello Vue3</div>
就框架而言,它需要找到前后差异的地方,然后再更新变化了的地方,而它所实现本次更新变化的代码仍然是
app.textContent = 'hello vue3'
倘若我们把直接修改的性能消耗定义为A,把找出前后差异所花费的性能消耗定义为B,则有如下关系:
- 命令式代码的更新性能消耗=A
- 声明式代码的更新性能消耗=B+A
也就是说,在更新时,声明式代码会比命令式代码多出 找出差异的性能消耗。 哪怕最理想的情况下,也就是哪怕找出差异所消耗的性能为0,也仅仅是能让声明式代码的性能消耗等于命令式代码的性能消耗,而无法做到比命令式消耗的更少。
会出现这个的原因,就是因为,框架本身就是封装了命令式代码才实现了面向用户的声明式。
命令式的代码性能更好,可是为什么Vue却还是选择了声明式的设计方案呢? 原因在于: 声明式代码的可维护性更强。
敲定设计方案之后,框架设计者要做的就是 在保持可维护性的同时,让性能损失最小化。
1.3 虚拟DOM的性能到底如何
在1.2中我们得到了一个关系式: 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。
一般来说,直接修改的性能消耗是固定的,即无法再优化了的,因此我们要让性能损失最小化,那就只能从找出差异的性能消耗入手了, 如果我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。
而 虚拟DOM 就是为了最小化找出差异这一步的性能消耗而出现的。
理论上, 采用虚拟DOM的更新技术是不可能比原生JS操作DOM要高的的,为什么说是理论上呢?因为,我们很难写出 绝对优化的命令式代码。
1.3.1 比较innerHTML和虚拟DOM的性能
为了比较两者的性能,我们需要先了解两者的创建、更新页面的过程。
1.3.1.1 创建页面
(1) innerHTML
对于innerHTML而言,创建页面前需要先构造一段html字符串。
const html = `<div><span>...</span></div>`
然后将这段html字符串赋值给DOM元素的innerHTML属性。
div.innerHTML = html
虽然代码很简单,但是为了渲染出页面要做的可不止这些, 首先要把html字符串解析成DOM树,这是一个DOM层面的计算。
通过innerHTML创建页面性能 = HTML字符串拼接的计算量 + innerHTML的DOM计算量
(2) 虚拟DOM
虚拟DOM将创建页面的过程分两步走:
- 第一步:创建JS对象,这个对象可以理解为是对真是DOM的描述;
- 第二部:递归地遍历虚拟DOM树,并创建真实的DOM。
通过虚拟DOM创建页面的性能 = 创建JS对象的计算量 + 创建真实DOM的计算量
(3)两者比较
对比两者创建页面时的计算量,可以发现其实两者的差距并不大, 我们从宏观的角度来看差异只看数量级上的差异,如果两者在同一个数量级,则认为没有差异。
既然创建页面的时候两者在性能消耗方面没有多大差异,那么我们接着来看更新页面的时候两者的性能消耗差异。
1.3.1.2 更新页面
(1)innerHTML
使用innerHTML更新页面的过程是 重新构建HTML字符串,再重新设置DOM元素的innerHTML属性。 也就是说,哪怕我们只是改了一个标点,也要重新设置innerHTML属性,而我们要知道的是, 重新设置innerHTML属性等价于销毁所有旧的DOM元素,再全量创建新的DOM元素。
就好比,一篇写好的文章,哪怕我们要改一个标点,都要换一张新的纸,重新把文章写一遍。
(2)虚拟DOM
虚拟DOM更新页面的时候,需要重新创建JS对象(新的虚拟DOM树),然后再比较新旧虚拟DOM,找到变化的元素并且替换它。
而这个比较算法就是Diff,因此可以说, 更新页面的时候,虚拟DOM在JS层面的运算要比创建页面时多出一个Diff的性能消耗,然后这个性能消耗毕竟也是JS层面的运输,所以不会产生数量级的差异。
两者比较
通过上面两者的更新页面的过程,我们可以看到,虚拟DOM的优势已经很明显了,它在更新页面时只需要更新必要的元素,而innerHTML则需要全量更新。
除此之外,在更新页面的时候,两者的性能影响因素是不一样的。
对虚拟DOM而言,无论页面有多大,都只会更新变化的内容,而对于innerHTML而言,页面越大,则其更新时的性能消耗就越大。
1.4 运行时和编译时
我们可以把框架架构分为3种: 纯运行时,运行时+编译时,纯编译时。
纯运行时的框架没有编译过程,因此我们无法分析用户提供的内容;
纯运行时的框架加入编译步骤,就是运行时+编译时,我们可以分析用户提供的内容,进入做出优化;
唇编译时框架,可以分析用户提供的内容,由于不需要使用任何运行时,而是直接编译成可执行的JS代码,因此性能更好,但是缺少灵活性。
Vue3采用的是运行时+编译时架构。