Vue.js 设计与实现 - 01 权衡的艺术

“框架设计里到处都体现了权衡的艺术”

深入探讨 Vue.js 3 各个模块的实现思路和细节之前,我认为有必要先来讨论视图层框架设计方面的内容。为什么呢?因为当我们设计一个框架的时候,框架本身各个模块之间并不是相互独立的,而是相互关联、相互制约的。作为框架设计者,一定要对框架的定位和方向拥有全局的把控,这样才能做好后续的模块设计和拆分。同样,作为学习中,我们在学习框架时,也应该从全局的角度对框架的设计拥有清晰的认知,否则很容易被细节困住,看不清全貌。

第一篇 框架设计概览 命令式(性能) 声明式(维护) 虚拟DOM 运行时 编译时#行编译时

1.1 命令式和声明式

从范式上来看,视图层框架通常分为命令式声明式,它们各有优缺点。作为框架的设计者,应该对两种范式都有足够的认知,这样才能做出正确的选择,甚至想办法集合两者的优点。

命令式

如 JQuery,命令式框架的一大特点就是关注过程,可以看到下面的代码中,自然语言描述能够与代码产生一一对应的关系,代码本身描述的是“做事的过程”。

1
2
3
4
5
6
// 获取 id 为 app 的 div 标签
// 设置它的文本内容
// 为其绑定点击事件,当点击时弹出提示:ok
$('#app')
.text('hello world')
.on('click', () => alert('ok'))

声明式

如 Vue,声明式框架的一大特点就是关注结果,代码本身描述的是“要做什么”,即结果,而不是“怎么做”。至于实现该结果的过程,则由 Vue.js 帮我们完成的,换句话说,Vue.js 帮我们封装了过程。因此,能够猜到 Vue.js 的内部实现是命令式的,暴露给用户的确是声明式。

1
<div @click="() => alert('ok')">hello world</div>

1.2 性能和可维护性的权衡

命令式和声明式各有优缺点,在框架设计方面,则体现在性能与可维护性之间的权衡 —— 声明式的代码不优于命令式代码的性能

拿上面的例子来说,假设把 div 标签的文本内容修改为 hello world2,还有什么其他代码比下面的代码性能更好呢?答案是“没有”

1
div.textContent = 'hello world2'

对于 Vue.js 来讲,为了实现最优的更新性能,它需要找出差异并只更新变化的地方,所以:

  • 命令式代码的更新性能消耗 = 直接修改的性能消耗
  • 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗

框架本身就是封装了命令式代码才实现了面向用户的声明式。

为什么 Vue.js 选择声明式的设计方案呢?

原因在于声明式的代码可维护性更强。

这体现了我们在框架设计上要做出的关于可维护性与性能之间的权衡。在采用声明式提升可维护性的同时,性能就会有一定的损失,而框架设计者要做的是:在保持可维护性的同时让性能损失最小化

1.3 虚拟 DOM 的性能到底如何

前面讲到的“声明式代码的更新性能消耗”,如果,我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能,而所谓的虚拟 DOM,就是为了最小化找出差异这一步的性能消耗而出现的。

1.4 运行时和编译时

当设计一个框架的时候,我们由三种选择:

  • 纯运行时
  • 纯编译时
  • 运行时 + 编译时

需要根据目标框架的特征,以及对框架的期望,做出合适的决策。为了做出合适的决策,需要清楚的知道什么是运行时,什么是编译时,它们各自的特征,对框架有哪些影响。

假设我们设计了一个框架,它提供了一个 Render 函数,用户可以为该函数提供一个树形结构的数据对象,然后 Render 函数会根据该对象递归地将数据渲染成 DOM 元素。

如果用户需要支持类似 HTML 标签的方式描述树形数据结构的数据对象呢?为了满足用户需求,编写了一个编译函数(Compiler),该函数可以将用户提供的内容编译成树形结构的数据对象,然后再调用 Render 函数渲染成 DOM 元素。

这时,我们的框架就变成了运行时 + 编译时的框架。它既支持运行时,用户直接提供数据对象而无需编译,也支持编译时,用户可以使用 HTML 标签的方式描述数据对象后再交给运行时处理。

准确的说,上面的代码其实是运行时编译,意思是代码运行时才开始编译,而这会产生一定的性能消耗,因此我们也可以在构建时就执行 Compiler 函数将用户提供的内容编译好,等到运行时就无须编译了,这对性能是非常友好的。

既然编译器可以把 HTML 字符串编译成数据对象,那么能不能直接编译成命令式代码呢?

1
2
3
4
5
// <div>hello world</div>
// 编译后
const div = document.createElement('div')
div.textContent = 'hello world'
document.body.appendChild(div)

这样我们就只需要一个 Compiler 函数就可以了,连 Render 都不需要,这就是一个纯编译时的框架。因为我们不需要支持任何运行时的内容,用户的代码需要通过编译器编译后才能实现。

纯运行时的框架,由于没有编译的过程,所以没法分析用户提供的内容,但是加入编译步骤,可以分析用户提供的内容,看看哪些内容未来会不会变化,这样我们可以在编译时提取这些信息,然后将其传递给 Render 函数,做进一步的优化。

1.5 总结

  • 命令式关注过程,声明式关注结果,命令式性能上可以做极致优化,但是用户要承受巨大的心智负担;声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲,框架的设计者要想办法尽量使性能损耗最小化。

  • 声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,虚拟 DOM 的意义在于找出差异的性能消耗最小化。

  • Vue.js 3 是一个编译时 + 运行时的框架,它在保持灵活性的基础上,还能通过编译手段分析用户提供的内容,进一步提升更新性能。