深入解析Vue与React如何将类HTML模板或JSX语法编译为底层JavaScript渲染逻辑与虚拟DOM,涵盖词法解析、AST转换、静态优化及代码生成的完整原理与代码对照。
Vue和React中HTML模板怎么编译为JS渲染逻辑
时间:2026-11-03
浏览:415
文章目录
一、核心机制差异:模板引擎与语法糖的本质分歧
在深入编译细节前,必须明确两者的底层定位差异:
- Vue 的 Template 是一门受约束的领域特定语言(DSL):虽然它的外观与普通 HTML 极为相似,但其语法受到严格限制(如
v-if、v-for、v-model等专用指令)。正因为规则封闭可预测,Vue 编译器能够在编译阶段做极尽深入的静态分析,精确识别哪些是永远不会改变的静态节点,哪些是包含动态绑定的变动字段。 - React 的 JSX 是一种 ECMAScript 语法扩展:JSX 本质上是嵌入在 JavaScript 代码中的结构表达,在 JSX 内部可以自由书写任意合法的 JS 表达式(如
list.map(...)、三元表达式、立即执行函数等)。因此,通用的转译器(如 Babel、SWC、esbuild)无法推断开发者在运行时的动态意图,它只负责机械地将尖括号语法还原为函数调用。
二、Vue 模板编译全过程拆解
Vue 3 的模板编译流程主要由 @vue/compiler-core 与 @vue/compiler-dom 驱动,分为三个连贯的标准阶段:词法与语法解析(Parse)、AST 转换与静态优化(Transform)、代码生成(Codegen)。
1. 词法与语法解析(Parse)
编译器接收字符串格式的 HTML 模板,利用状态机逐字符扫描(Tokenizing)。当遇到 < 时判定为标签起始,解析标签名、属性列表;遇到 {{ 时识别为插值表达式;遇到文本则提取纯字符。最终将一维文本转换为一棵描述父子关系的原始 AST 树。每个 AST 节点包含类型(NodeTypes.ELEMENT、NodeTypes.INTERPOLATION 等)、标签名、属性指令集合以及子节点列表。
2. AST 转换与静态优化(Transform)
这是 Vue 编译期最核心的步骤。编译器在此阶段并不急于输出代码,而是递归遍历 AST 节点,挂载各种插件处理器(如 transformElement、transformVFor、transformVIf),并在节点上做三项关键优化标记:
- 静态提升(hoistStatic):对于完全没有引用组件实例响应式变量的静态节点(如纯文本段落、静态样式属性),Vue 将它们提取到
render函数之外作为常量,组件重新渲染时直接复用,免除反复创建 VNode 的内存与计算开销。 - 补丁标记(PatchFlag):分析节点的动态依赖类型。如果是文字动态绑定,标记为
1 /* TEXT */;如果是class动态绑定,标记为2 /* CLASS */;如果是style,标记为4 /* STYLE */;若是动态属性键名则标记8 /* PROPS */。运行时对比两个新旧 VNode 时,Vue 仅需使用位运算(如patchFlag & PatchFlags.TEXT)就能精准更新对应部分,直接跳过无改动属性的比对。 - Block 树结构:常规虚拟 DOM diff 需要深度递归遍历所有整树节点。Vue 3 将包含动态节点的容器封装为一个“Block”。Block 内部维护一个一维数组
dynamicChildren,仅记录其深层后代中的动态节点。当组件状态更新时,diff 算法跳过所有静态层级,仅针对dynamicChildren线性循环比对。
3. 代码生成(Codegen)
在完成 AST 变换并附带 codegenNode 属性后,Codegen 模块通过构建调用上下文,将带有优化信息的 AST 节点翻译为最终可执行的 JavaScript 代码字符串。
完整编译前后对照示例
假定我们有一段 Vue 模板:
<template>
<div class="card">
<h3>用户详情</h3>
<p :id="userId">{{ userName }}</p>
<button @click="handleClick">点击刷新</button>
</div>
</template>
经过 Vue 3 编译器处理后,生成的底层 JavaScript 渲染逻辑如下:
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"
// 静态内容被提升为模块级全局常量,避免每次 re-render 重复分配内存
const _hoisted_1 = { class: "card" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("h3", null, "用户详情", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
// 动态属性与插值,标注了 PatchFlag:9 (TEXT, PROPS) 与动态属性列表 ["id"]
_createElementVNode("p", { id: _ctx.userId }, _toDisplayString(_ctx.userName), 9 /* TEXT, PROPS */, ["id"]),
// 事件监听函数被 _cache 缓存,避免每次渲染都生成新闭包导致子组件无效刷新
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "点击刷新")
]))
}
render 函数接受 _ctx 作为上下文,模板中声明的 userId、userName 自动代理至 _ctx.userId;而 _hoisted_2 的标志位 -1 告诉运行时 diff 算法:此节点为永久静态,跳过任何对比。
三、React JSX 编译为 JavaScript 渲染逻辑的过程
React 社区并没有设计独立的“模板”文件格式,而是采用 JSX 这种书写形式。JSX 在构建期由 Babel、SWC 或 TypeScript 编译器转换为标准的 JavaScript 表达式。
1. 经典运行时与现代自动运行时
在 React 17 之前,JSX 会被编译为 React.createElement(...) 调用,因此每个包含 JSX 的文件顶部都必须显式写上 import React from 'react'。而在 React 17 及之后,React 引入了全新的 JSX Transform(自动运行时),编译器会把 JSX 直接编译为从 react/jsx-runtime 导出的 _jsx 或 _jsxs 函数:
- 单个子节点时调用
_jsx(type, props, key)。 - 多个静态子节点时调用
_jsxs(type, props, key),底层避免了将子节点展开为可变参数数组(arguments)带来的运行时包装开销。
完整编译前后对照示例
假定有一段 React 组件:
function UserProfile({ id, name, onRefresh }) {
return (
<div className="card">
<h3>用户详情</h3>
<p id={id}>{name}</p>
<button onClick={onRefresh}>点击刷新</button>
</div>
);
}
在 modern JSX runtime 配置下,经 Babel(@babel/plugin-transform-react-jsx)编译后输出的纯 JS 逻辑为:
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function UserProfile({ id, name, onRefresh }) {
return _jsxs("div", {
className: "card",
children: [
_jsx("h3", { children: "用户详情" }),
_jsx("p", { id: id, children: name }),
_jsx("button", { onClick: onRefresh, children: "点击刷新" })
]
});
}
可以清楚地观察到:Babel 对 JSX 的处理是纯语法的扁平置换。它并不知道 h3 节点内部是永远不变的静态文本,因此只要 UserProfile 重新执行,_jsx("h3", ...) 就会无条件重新执行一次,返回一个新的纯 JS 对象(React Element)。
四、编译产物在运行时的比对与性能差异
编译器的不同定位,直接决定了 Vue 和 React 在运行时的 DOM 更新工作方式。
对比可知,两者的分工边界截然不同:
- Vue 把工作重心前置到编译期:在打包构件阶段,通过繁琐的词法解析和 AST 分析,把各种运行时线索(静态标记、靶向属性位、Block 集合)打包进
render函数中,运行时代码极为精简,几乎只需执行直截了当的位运算与按需补丁。 - React 把灵活性留给 JavaScript,将优化交给运行时与专业优化编译器:常规 JSX 编译器只负责语法降级;但为了缓解每次全量重跑函数所带来的性能成本,React 团队在 React 19 中推出了 React Compiler(前身是 React Forget)。React Compiler 属于深度分析 JS 语义的中间编译器,它会在编译期自动推断依赖并自动插入类似
useMemo的记忆化缓存,从而在保留 JSX 原生灵活性之余,达到接近 Vue 的精准渲染表现。
五、全景对照与技术选型认知
为了更直观地梳理两者从模板源文件到浏览器 DOM 的各个环节,下表对关键特征进行了横向比对:
| 对比维度 | Vue 3 Template 模式 | React JSX 模式 |
|---|---|---|
| 语法本质 | 严格受限的声明式 HTML 模板 DSL | 带尖括号语法的原生 JavaScript 语法扩展 |
| 编译时机 | 构建期(Vite/Webpack)或浏览器运行时专用构建包 | 纯构建期(Babel/SWC/esbuild),无运行时编译 |
| 编译输出产物 | 包含 PatchFlag、Block Tree 的 render(_ctx) 函数 |
递归嵌套的 _jsx() / createElement() 函数调用 |
| 静态提升能力 | 原生内置(自动将静态子树与静态属性提升为常量) | 默认无(依靠新版 React Compiler 做自动记忆化) |
| 运行期 Diff 粒度 | 靶向定位:通过 Block 数组直达动态节点,跳过静态树 | 全量遍历:递归比较 Fiber 节点及其属性 |
| 开发灵活性 | 受指令约束,但心智模型统一,语法语义明确 | 拥有完整的 JS 编程能力,但对开发者性能把控要求较高 |
总结
综上所述,Vue 和 React 将“类 HTML 标记”转化为“JS 渲染逻辑”的根本差异,根植于它们对待模板语法的态度:Vue 采用闭环的 DSL 换取深度的编译期分析与极高的运行时执行效率,将静态标记和优化路径硬编码到渲染函数中;而 React 则坚守 JavaScript 的表达力与语法直译,保持构建环节的简单直观,将协调优化放在运行时机制或更高维度的全局语义编译器上。理解这两条不同的技术路径,有助于我们在日常架构设计和性能调优中做出更加透彻的技术决策。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。