Skip to content

L38 · Vapor Mode:直接驱动 DOM 的编译模式 ​

🎯 本节目标:理解 Vue Vapor Mode 的设计目标、编译策略与适用边界
📦 本节产出:固定版本编译输出 + 纯 Vapor 与 VDOM 互操作实验
🔗 前置钩子:L33-L34 的 Virtual DOM + 编译器优化
🔗 后续钩子:Phase 4 完结,进入 D01–D15 深度专题

1. 什么是 Vapor Mode ​

Vapor Mode 将支持的 SFC 模板编译为直接驱动 DOM 的代码,其自身渲染路径不创建 VNode 树。 本课固定使用 Vue 3.6.0-rc.9(预发布版,2026-09-23 核对);L31–L37 的 3.5.43 源码结论不直接套用到这个版本。实验放在独立目录,不升级前面商城项目。固定版本 release

编译器把模板中的 DOM 结构和绑定关系提前转成代码,运行时保留响应式依赖、DOM 引用和结构控制逻辑。动态分支、列表与组件仍需要运行时支持,不能把“没有 VNode”理解为“没有运行时”。


2. 编译输出对比 ​

使用同一个模板,并让两种编译器都固定在 3.6.0-rc.9。以下是模板编译 API 的输出;完整 SFC 还包含脚本编译与组件包装。§6 的脚本可重现这些输出:

html
<div class="counter"><h1>{{ title }}</h1><p>Count: {{ count }}</p><button @click="count++">+1</button></div>

传统 VDOM 模式 ​

@vue/compiler-dom 使用 { mode: 'module', hoistStatic: true }:

javascript
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "counter" }
const _hoisted_2 = ["onClick"]

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
    _createElementVNode("p", null, "Count: " + _toDisplayString(_ctx.count), 1 /* TEXT */),
    _createElementVNode("button", {
      onClick: $event => (_ctx.count++)
    }, "+1", 8 /* PROPS */, _hoisted_2)
  ]))
}

这里仍有 Block/PatchFlag 优化;其他模板还可能命中静态缓存,不能把 VDOM 路径概括为每次重建并比较整个应用。

Vapor Mode ​

@vue/compiler-vapor 使用 { mode: 'module', prefixIdentifiers: true }:

javascript
import { child as _child, next as _next, txt as _txt, on as _on, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div class=counter><h1> </h1><p> </p><button>+1", 1)

export function render(_ctx) {
  const n3 = t0()
  const n0 = _child(n3)
  const n1 = _next(n0)
  const n2 = _next(n1)
  const x0 = _txt(n0)
  const x1 = _txt(n1)
  _on(n2, "click", () => (_ctx.count++))
  _renderEffect(() => {
    _setText(x0, _toDisplayString(_ctx.title))
    _setText(x1, "Count: " + _toDisplayString(_ctx.count))
  })
  return n3
}

模板工厂创建结构,生成代码取得节点并注册事件、响应式更新。这份结果把 title 与 count 两个文本更新放进同一个 renderEffect,因此绑定与 effect 并非总是一一对应。分组策略与 helper 名称属于这个 RC 的实现,不是应用应该直接调用的稳定接口。

两条路径都需要依赖追踪、更新调度和清理。Vapor 省去的是其渲染路径中的 VNode 构造与树形 patch 中间层。


3. 性能对比 ​

3.1 运行时开销 ​

工作传统 VDOMVapor Mode
更新表示VNode、缓存与动态节点信息DOM 引用、绑定及结构 block
内容更新render 后由 patch 决定 DOM 修改响应式更新调用生成的 DOM 操作
列表重排keyed diff 协调节点专门的列表逻辑协调条目与 DOM
内存与清理VNode、组件状态、依赖等DOM 引用、作用域、依赖及列表记录等
性能结论取决于模板、更新类型和编译优化可能减少中间工作,仍须测量具体场景

没有 VNode 不等于只有 DOM 引用或 GC 成本接近零;两种模式都创建 JavaScript 对象,也都要管理资源。

3.2 包体积 ​

纯 Vapor 的 createVaporApp 可以避免引入 VDOM runtime;加入互操作插件或 VDOM 组件后会带回相应代码。不要把某个最小示例的 gzip 大小当作固定的“Vue 运行时大小”。

比较时应固定依赖锁文件、生产模式、相同页面功能、压缩方式和加载边界,分别记录纯 Vapor、纯 VDOM、混合版本的产物;§6 的纯版与混合版用于验证接线,二者功能不同,不能直接用于性能排名。测量流程沿用 L30。

3.3 根据功能选择模式 ​

需求判断依据
大量 keyed 列表增删两种模式都有列表协调;用目标数据规模与操作序列实测
非 DOM 自定义渲染器本课的 Vapor 面向 DOM,VDOM 的 custom renderer 是另一条路径
render 函数 / JSX仍是 VDOM 组件;在 Vapor 应用内使用需互操作
Options API 组件不属于 Vapor 支持的组件写法,可留在 VDOM 区域
单点高频更新可测试 Vapor 是否减少渲染工作,不能直接保证应用更快
无交互的静态页面先判断是否需要客户端运行时,不因静态就必选 Vapor

4. Vapor 处理条件和列表 ​

4.1 v-if ​

html
<div v-if="show">Hello</div><div v-else>Bye</div>

固定版本输出把条件与两个分支工厂交给 createIf:

javascript
import { createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<div>Hello", 3)
const t1 = _template("<div>Bye", 3)

export function render(_ctx) {
  const n0 = _createIf(() => (_ctx.show), () => {
    const n2 = t0()
    return n2
  }, () => {
    const n4 = t1()
    return n4
  }, 357 /* TRUE_SINGLE_ROOT, FALSE_SINGLE_ROOT, TRUE_NO_SCOPE, FALSE_NO_SCOPE, KEYED_INDEX_0 */)
  return n0
}

createIf 管理分支切换及关联作用域;真实分支可能不止一个 DOM 节点。上面的数字是编译器内部标志,只用于观察,不要求记忆或手写。

4.2 v-for ​

html
<div v-for="item in list" :key="item.id">{{ item.name }}</div>

此模板生成 createFor,传入列表 getter、每项的创建逻辑与 item => item.id 键函数。它仍要识别保留、新增、移除、移动的条目;“没有 VDOM diff”不等于“列表不用协调”。用稳定 key 保留条目身份的原则仍成立。§6 的重排按钮可验证 DOM 节点复用,修改名称则观察条目内的响应式绑定。


5. 与其他框架的对比 ​

这里比较渲染路径,不给没有同条件测量的体积与速度排名:

方案与编译和更新相关的特点
React 19组件 render 描述界面,由协调器处理更新;React Compiler 1.0 已稳定,可加入编译期 memoization
Vue VDOM模板编译提供静态缓存、PatchFlag 与 Block 信息,运行时仍使用 VNode
Svelte 5编译器处理模板与 runes,运行时响应式支持仍存在,不是“完全消除框架”
Solid细粒度响应式依赖驱动 DOM 更新,组件与响应式计算的执行范围不同
Vue 3.6 RC Vapor编译器生成 DOM 与响应式更新代码,支持 Vue API 的一个子集

React 不是“纯运行时、每次全量 diff”,Svelte 5 也不能只用旧式赋值转换概括。依据:React Compiler 1.0、Svelte runes、Solid 细粒度响应式。


6. 当前状态与使用方式 ​

3.6.0-rc.9 仍是预发布版本。官方 RC 说明包含 API 子集、事件委托、插槽与互操作边界;这里验证计数、分支、keyed 列表以及标准 props/events,不据此推断所有第三方组件库都兼容。RC 功能与互操作说明

6.1 建立独立实验 ​

使用 Node 22.12+(22.x),在课程仓库外创建 vue-vapor-lab,把以下文件保存到对应路径。版本固定便于重复编译实验;后续更新 RC 时应一起核对 compiler/runtime:

bash
mkdir vue-vapor-lab
cd vue-vapor-lab
mkdir src
json
// package.json(保存时删除此注释)
{
  "name": "vue-vapor-lab",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite --host 127.0.0.1 --port 5176 --strictPort",
    "typecheck": "vue-tsc --noEmit",
    "build": "vite build",
    "build:interop": "vite build --mode interop --outDir dist-interop",
    "compile": "node compile.mjs"
  },
  "dependencies": { "vue": "3.6.0-rc.9" },
  "devDependencies": {
    "@vue/compiler-dom": "3.6.0-rc.9",
    "@vue/compiler-vapor": "3.6.0-rc.9",
    "@vitejs/plugin-vue": "6.0.9",
    "@types/node": "22",
    "typescript": "5.9.3",
    "vite": "6.4.3",
    "vue-tsc": "3.3.11"
  },
  "overrides": { "vue": "$vue" }
}

plugin-vue 的 Vue peer 范围尚未包括预发布版;这个独立实验用 overrides 明确选择上面声明的 RC,不使用 force 或忽略整个依赖树的校验。保存 npm install 生成的 lockfile。

json
// tsconfig.json(保存时删除此注释)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "types": ["vite/client", "node"]
  },
  "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => ({
  plugins: [vue()],
  build: { rollupOptions: { input: mode === 'interop' ? 'interop.html' : 'index.html' } },
}))

Vapor 标记写在 SFC 上,插件沿用 vue() 默认配置。与 L35 无插件的实验不同,这里由 Vue 插件处理编译标志。

html
<!-- index.html -->
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>纯 Vapor 实验</title></head>
<body><div id="app"></div><script type="module" src="/src/main.ts"></script></body>
</html>
typescript
// src/main.ts
import { createVaporApp } from 'vue'
import App from './App.vue'
createVaporApp(App).mount('#app')
vue
<!-- src/App.vue -->
<script setup lang="ts" vapor>
import { ref } from 'vue'
withDefaults(defineProps<{ label?: string }>(), { label: '纯 Vapor' })
const emit = defineEmits<{ change: [value: number] }>()
const count = ref(0)
const show = ref(true)
const list = ref([
  { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 3, name: 'Cherry' },
])
let nextId = 4
function increment() { emit('change', ++count.value) }
function addItem() { list.value.push({ id: nextId++, name: 'New item' }) }
</script>

<template>
  <main>
    <h1>{{ label }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="show = !show">切换分支</button>
    <p v-if="show">Hello</p><p v-else>Bye</p>
    <button @click="list = [...list].reverse()">反转列表</button>
    <button @click="addItem">新增条目</button>
    <button :disabled="list.length === 0" @click="list = list.slice(1)">删除首项</button>
    <ul>
      <li v-for="item in list" :key="item.id" :data-id="item.id">
        <input v-model="item.name" :aria-label="`条目 ${item.id}`">
        <span>{{ item.name }}</span>
      </li>
    </ul>
  </main>
</template>

6.2 在 VDOM 应用嵌入 Vapor ​

增加下面三个文件。普通 VDOM 根组件通过 props 与 events 和 Vapor 子组件通信,入口必须安装 vaporInteropPlugin:

html
<!-- interop.html -->
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vapor 互操作实验</title></head>
<body><div id="app"></div><script type="module" src="/src/main-interop.ts"></script></body>
</html>
typescript
// src/main-interop.ts
import { createApp, vaporInteropPlugin } from 'vue'
import VdomApp from './VdomApp.vue'
createApp(VdomApp).use(vaporInteropPlugin).mount('#app')
vue
<!-- src/VdomApp.vue:没有 vapor 标记 -->
<script setup lang="ts">
import { ref } from 'vue'
import App from './App.vue'
const lastCount = ref(0)
const label = ref('VDOM 父组件传入的标题')
const visible = ref(true)
</script>
<template>
  <p>父组件收到:{{ lastCount }}</p>
  <button @click="label = '标题已更新'">修改子标题</button>
  <button @click="visible = !visible">挂载或卸载子组件</button>
  <App v-if="visible" :label="label" @change="lastCount = $event" />
</template>

反向在 Vapor 根应用使用 VDOM 子组件也要安装同一个插件,并会带入 VDOM runtime。Options API 与 JSX/render function 不能直接成为 Vapor 组件;已有 VDOM 应用需要逐个确认组件用到的 API,再决定迁移范围。

6.3 重现编译输出并运行 ​

javascript
// compile.mjs
import { compile as compileVDOM } from '@vue/compiler-dom'
import { compile as compileVapor } from '@vue/compiler-vapor'
const templates = [
  '<div class="counter"><h1>{{ title }}</h1><p>Count: {{ count }}</p><button @click="count++">+1</button></div>',
  '<div v-if="show">Hello</div><div v-else>Bye</div>',
  '<div v-for="item in list" :key="item.id">{{ item.name }}</div>',
]
for (const template of templates) {
  console.log('Template:', template)
  console.log('VDOM:', compileVDOM(template, { mode: 'module', hoistStatic: true }).code)
  console.log('Vapor:', compileVapor(template, { mode: 'module', prefixIdentifiers: true }).code)
}
bash
npm install
npm run typecheck
npm run compile
npm run build
npm run build:interop
npm run dev

打开 http://127.0.0.1:5176/ 测试计数、分支、列表增删与重排;在输入框改名,文本应同步变化,重排时该 id 的 DOM 节点应被复用。再打开 /interop.html,检查子组件事件更新父状态、父 props 更新标题、卸载后重建子计数回到 0。构建成功只说明代码可打包,这些交互还需要浏览器验证。


7. Phase 4 总结 ​

知识链路 ​

用户代码 → 响应式读取(ref 访问器 / Proxy)→ 收集依赖 → 数据变化 → 通知
→ scheduler 排队 → 微任务执行 → render 生成 VNode
→ diff 找到变化 → patch 操作 DOM → 浏览器渲染 → 用户看到更新

Vapor 简化为:
响应式读取 → 依赖订阅 → 数据变化 → 调度相关更新 → DOM 操作 → 浏览器渲染

Git 提交 ​

bash
git add .
git commit -m "L38: Vapor Mode 与 Phase 4 实验"
git tag phase-4-complete

🔬 深度专题 ​

📖 D15 · Vapor Mode 原理 — 没有虚拟 DOM 的 Vue 还是 Vue 吗?