框架的运行时和编译时

设计框架时有三种选择:纯运行时、运行时 + 编译时或纯编译时。

纯运行时

假设框架提供一个 Render 函数,用户为函数提供一个树形结构的数据对象,Render 函数会根据该对象递归地将数据渲染成 DOM 元素。
规定对象有两个属性:tag 代表标签名称, children 既可以是一个数组(代表字节点),也可以是字符串(代表文本子节点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}

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

// 用户使用
Render(obj, document.body)

用户在使用 Render 函数渲染内容时,直接为其提供一个树形结构数据对象,运行后便会渲染对应的 DOM 元素。这就是一个纯运行时的框架实现。

运行时 + 编译时

手写树形结构的数据对象太麻烦,且不直观,希望用类似于 HTML 标签的方式描述树形结构的数据对象。
为满足需求,需要引入编译的手段,把 HTML 标签编译成树形结构的数据对象,就可以继续使用 Render 函数了。

编写一个 Compiler 程序,它的作用就是把 HTML 字符串编译成树形结构的数据对象。用户使用时分别调用 Compiler 函数和 Render 函数:

1
2
3
4
5
6
7
8
9
const html = `
<div>
<span>hello world</span>
</div>
`
// 调用 Compiler 编译得到树型结构的数据对象
const obj = Compiler(html)
// 再调用 Render 进行渲染
Render(obj, document.body)

上面的实现就是一个运行时 + 编译时的框架。既支持运行时,用户直接提供树形结构的对象;也支持编译时,用户提供 HTML 字符串。
准确的说,上面代码其实是运行时编译,代码运行的时候才开始编译。
可以在构建的时候就执行 Compiler 将用户提供的内容编译好,优化性能。

纯编译时

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

这样只需要一个 Compiler 函数就可以了,变成一个纯编译时的框架,不支持任何运行时的内容,用户代码要通过编译器编译后才能运行。

总结

上面用简单的例子理解了框架设计层面的运行时运行时 + 编译时以及编译时。 纯运行时的框架,由于没有编译的过程,无法分析用户提供的内容;
如果加入编译步骤,可以分析用户提供的内容,将分析得出的信息传递给 Render 函数,Render 函数就可以做进一步优化了;
纯编译时的框架,也可以分析用户提供的内容,由于不需要任何运行时,直接编译成可执行的 JavaScript 代码,性能可能会更好,但是灵活性不好,用户提供的内容必须编译后才能使用。

Vue.js3 保持了运行时 + 编译时的架构,在保持灵活性的基础上尽可能优化。