Vue和React中HTML模板怎么编译为JS渲染逻辑

作者:微光时间:2026-11-03 09:03:37浏览:417

深入解析Vue与React如何将类HTML模板或JSX语法编译为底层JavaScript渲染逻辑与虚拟DOM,涵盖词法解析、AST转换、静态优化及代码生成的完整原理与代码对照。

在现代前端工程中,Vue的类HTML模板和React的JSX语法都不是浏览器能够直接解析的原生代码。它们最终都必须经过编译工具链的语法转换,输出为纯JavaScript函数调用,在运行时生成虚拟DOM节点(VNode),再由框架的渲染器映射到真实的DOM树上。本文将具体剖析两套框架从输入标记到输出JS渲染逻辑的完整转换链路、代码对照以及设计哲学差异。
Vue和React中HTML模板怎么编译为JS渲染逻辑
图 1:Vue 与 React 模板编译为 JavaScript 渲染逻辑的整体路线

一、核心机制差异:模板引擎与语法糖的本质分歧

在深入编译细节前,必须明确两者的底层定位差异:

  • 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)。

Vue和React中HTML模板怎么编译为JS渲染逻辑
图 2:Vue 3 模板编译三阶段(词法解析、AST变换、代码生成)流水线

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)))
    }, "点击刷新")
  ]))
}
注意事项:Vue 的 render 函数接受 _ctx 作为上下文,模板中声明的 userId、userName 自动代理至 _ctx.userId;而 _hoisted_2 的标志位 -1 告诉运行时 diff 算法:此节点为永久静态,跳过任何对比。

三、React JSX 编译为 JavaScript 渲染逻辑的过程

React 社区并没有设计独立的“模板”文件格式,而是采用 JSX 这种书写形式。JSX 在构建期由 Babel、SWC 或 TypeScript 编译器转换为标准的 JavaScript 表达式。

Vue和React中HTML模板怎么编译为JS渲染逻辑
图 3:React JSX 经由转译器到虚拟 DOM 运行时对象的转化链路

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和React中HTML模板怎么编译为JS渲染逻辑
图 4:编译阶段注入的信息量差异直接塑造了不同的运行时 Diff 复杂度

对比可知,两者的分工边界截然不同:

  • 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渲染逻辑
图 5:Vue 与 React 模板编译架构理念总结矩阵

总结

综上所述,Vue 和 React 将“类 HTML 标记”转化为“JS 渲染逻辑”的根本差异,根植于它们对待模板语法的态度:Vue 采用闭环的 DSL 换取深度的编译期分析与极高的运行时执行效率,将静态标记和优化路径硬编码到渲染函数中;而 React 则坚守 JavaScript 的表达力与语法直译,保持构建环节的简单直观,将协调优化放在运行时机制或更高维度的全局语义编译器上。理解这两条不同的技术路径,有助于我们在日常架构设计和性能调优中做出更加透彻的技术决策。

作者最新文章
相关文章
更多