本文主要阐述前端框架中的运行时 和编译时 的区别。首先说明本文内容参考书籍 《Vue.js 设计与实现》,并加以自己的理解创作。有错误的地方还请指出。
运行时
运行时就是指代码实际执行时的阶段。前端代码是在浏览器中执行的,换言之,如果一个框架的代码可以直接在浏览器中执行,那它就是一个纯运行时的框架。
举个例子,假设我们设计了一个框架,它提供一个 Render 函数,用户使用时,为该函数提供一个描述 DOM 树形结构的参数对象,然后 Render 函数根据该对象递归地将数据渲染成 DOM 元素。假设规定要传入的对象结构如下:
javascript
const obj = {
tag: "div",
children: [
{
tag: "span",
children: "hello world!",
},
],
};
对象中有两个属性:tag 代表标签名称,children 可以是数组(代表子节点)也可以是文本(代表文本子节点)。然后实现 Render 函数:
javascript
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) {
obj.children.forEach((child) => Render(child, el));
}
root.appendChild(el);
}
现在可以在浏览器环境中调用 Render 函数:
javascript
// 渲染到 body 下
Render(obj, document.body);
此时就会看到预期的结果。这就是一个纯运行时的框架例子,因为浏览器可以直接运行 JavaScript 文件,所以它不需要任何的转换操作。
编译时
了解了运行时,那编译时就很好理解,它是指将源代码转换成可执行代码的阶段。在前端中,最简单的例子就是将高级的 ES6 语法转换为浏览器可以理解和执行的 ES5 语法,这个过程也是编译。
还是拿上面的代码示例来说明,如何将它改造成编译时的框架?但是首先我们需要知道,不是为了有编译时而加编译时,而是为了解决问题才有的编译时。
我们来看上面运行时的代码中有什么问题?很显然就是定义 DOM 树形结构对象有点麻烦,如果 DOM 结构很复杂,需要定义的对象就会嵌套很深,抽象不直观。所以我们在想可以直接像写 HTML 一样来声明 UI 吗?对,就是 vue 中模板(template)那样编写。首先我们应该知道,vue 文件中的 template 部分并不是 html,虽然长得很像,但是浏览器是不能直接识别它们的,所以 template 模板是需要编译的。那么编译成什么呢?
编译的结果是为了让浏览器能直接运行,那么假设我们的模板这样写:
html
<div>
<span>hello world!</span>
</div>
记住它不是 html 文件,所以我们需要将它编译成命令式代码:
javascript
const div = document.createElement("div");
const span = document.createElement("span");
span.innerText = "hello world!";
div.appendChild(span);
document.body.appendChild(div);
此时浏览器就可以直接执行代码并达到预期效果了。将这个过程封装成一个编译器,用户所有的代码只能通过编译器编译后才能执行,那它就是一个纯编译时框架,代表框架如 Svelte
。
运行时 + 编译时
上面提到 vue 中的模板需要编译才能被浏览器执行,那 vue 是一个编译时框架吗?答案是否的。
vue 是一个运行时 + 编译时的框架。这个在官方文档中有提及运行时 vs 编译时响应性,文档中说到:
Vue 的响应式系统基本是基于运行时的。追踪和触发都是在浏览器中运行时进行的。
这里先不管 vue 中的实现细节。我们还是拿上面的例子说明,如果它是运行时+编译时,应该怎么实现?
其实就是加一个编译器就行,但不是直接编译成命令式创建代码,而是将模板编译成一个 DOM 描述对象,然后将对象传入渲染器。就是这样:
html
<div>
<span>hello world!</span>
</div>
编译成 ⬇️
javascript
const obj = {
tag: "div",
children: [
{
tag: "span",
children: "hello world!",
},
],
};
然后渲染:
javascript
Render(obj, document.body);
这就是一个运行时+编译时的过程,准确的说是一个运行时编译,就是代码执行时才编译。在实际框架使用中,我们可以在构建过程中执行编译,等真正运行时就无需编译,这样性能更好。
代码写到这,我们可能会想这样的运行时编译是不是多此一举,直接编译彻底不是更省事吗?理论上纯编译时的性能确实会更好,不需要运行时参与,代码直接编译成可执行的 JavaScript 代码,但是这也散失了部分灵活性,用户的内容必须编译后才能使用。而纯运行时,由于没有编译过程,就没办法分析用户提供的内容,从而不能做对应的优化,一般编译器中我们可以实现语法分析,优化和转换,可以执行语法错误检查,模块打包和代码压缩等任务。只能说框架作者在设计框架时,都是有自己的取舍,不能直接判断哪种更好。
使用框架 or 直接写 html
看到上面分析的一堆,我们不禁会想抛弃框架直接写 html 不是更直观省事。但事实我们也感受到了,使用 vue 或 react 这样的现代框架,能够大大增加我们的开发效率。原因就在于这些框架帮我们封装好了做事的过程,如果是传统原生 html 开发,我们需要维护实现目标的整个过程,包括要手动完成 DOM 元素的创建、更新、删除等工作。但框架让我们只需要为结果声明 UI,不需要关心实现过程。这就是框架的便利之处,了解运行时和编译时也是为了更好的了解框架的实现原理。
结尾
最后对运行时 和编译时做一个总结。
运行时(Runtime)是代码实际执行时的阶段。在前端框架中,运行时通常指在浏览器环境中加载和执行已经编译好的代码。这个阶段包括了解析 HTML 结构、构建 DOM 树、执行 JavaScript 代码以及处理用户交互等任务。在运行时,前端框架会利用编译时生成的代码来动态地更新和渲染页面,处理事件和数据的变化等。
编译时(Compile Time)是指在开发过程中,将开发者编写的源代码转换为可执行的代码的阶段。例如 es6 转 es5,vue 模板转成 html 等。在编译时,前端框架的编译器会对代码进行语法分析、优化和转换,以生成可在浏览器中运行的代码。编译时的任务包括语法检查、模块打包、代码压缩等。