> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 模板编译与渲染

> 拆解模板到渲染函数的三阶段编译，讲清 v-model 编译产物、nextTick 调度与 script setup 的版本边界。

> 我最早写模板只当它是语法糖，后来查过 nextTick 和 v-model 的编译产物，才明白模板到 DOM 中间隔着一层编译：PatchFlag 和静态提升都发生在这里。

***

## Q1: 模板是怎么变成真实 DOM 的？

**1. 模板是语法糖**

你写的 `<template>` 不是真实 DOM，也不是虚拟 DOM--它是一种**DSL（领域特定语言）**。Vue 的编译器会把它转换成 JavaScript 函数（渲染函数），这个函数返回虚拟 DOM 树。

**2. 编译器的工作**

编译器做三件事：**解析**（把模板字符串变成 AST）、**转换**（对 AST 做优化标记，如 PatchFlag、静态提升）、**生成**（把 AST 变成渲染函数代码）。

**3. 运行时的工作**

渲染函数执行时创建虚拟 DOM 树。数据变化时重新执行渲染函数得到新的虚拟 DOM 树，用 diff 算法比较新旧树，把差异应用到真实 DOM。

**4. 编译时优化：PatchFlag 与 Block Tree**

Vue 3 的编译器在编译阶段为每个动态节点打上 **PatchFlag** 标记，告诉运行时"这个节点的哪些部分会变化"--比如只有文本变化（TEXT）、只有 class 变化（CLASS）。配合 **Block Tree** 结构，运行时只需遍历有标记的节点，跳过所有静态节点，将 diff 的开销降到最低。

**5. 关键启示: 编译时 + 运行时的协同优势**

Vue 的模板编译策略证明了一个设计理念：**让编译器做尽可能多的优化，减轻运行时的负担**。比起 React 的 JSX（运行时才做 diff），Vue 的模板在编译时就掌握了结构信息，这使得 Vue 3 在大型应用中的更新性能通常优于"纯运行时"方案。面试中说清这一点，比单纯背诵编译流程更能打动对方。

**模板编译解决的核心问题：**

```text theme={null}
问题：开发者不想手写 DOM 操作，但浏览器只认识真实 DOM

手写 DOM 操作 → 繁琐、容易出错、难以维护
直接操作真实 DOM → 无法批量优化、无法跨平台

解决：模板 → 编译器 → 渲染函数 → 虚拟 DOM → diff → 真实 DOM

模板编译的完整流水线：

  ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  模板字符串 │ →   │   parse   │ →   │ transform │ →   │ generate  │ →   │ 渲染函数   │
  │ '<div>...'│     │ 模板→AST  │     │ AST 优化   │     │ AST→代码   │     │ h('div')  │
  └──────────┘     └──────────┘     └──────────┘     └──────────┘     └──────────┘
                                                     ↑
                                          ┌──────────┴──────────┐
                                          │ 优化标记              │
                                          │ • PatchFlag (动态标记)│
                                          │ • 静态提升 (hoist)    │
                                          │ • Block Tree          │
                                          └─────────────────────┘

  运行时：
  ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
  │  渲染函数   │ →   │ 虚拟 DOM  │ →   │   diff    │ →   │ 真实 DOM  │
  │ 执行→VNode │     │ 新旧两棵树 │     │ 对比差异   │     │ 最小更新   │
  └──────────┘     └──────────┘     └──────────┘     └──────────┘
```

> [!NOTE]
>
> 模板是给开发者用的语法糖，编译器把它变成渲染函数，运行时负责把渲染函数的输出变成真实 DOM。

> [card] 手写渲染函数
>
> 用 `h('div', { class: 'app' }, [...])` 描述 UI，代码冗长难以维护。就像用机器码写程序--你能做到，但效率极低，而且编译器无法帮你优化。

> [card] 模板语法
>
> 用 `&lt;div class="app"&gt;...&lt;/div&gt;` 描述 UI，简洁直观。就像用高级语言写程序--编译器帮你做 PatchFlag、静态提升、Block Tree 等优化，你只管描述"要什么"。

> [!TIP]
>
> 模板编译就像一个"菜谱翻译官"。你用中文写菜谱（模板语法），翻译官把它翻译成英文（渲染函数），外国厨师（浏览器）按英文菜谱做菜（渲染 DOM）。翻译官还会在菜谱上标注"这道菜的口味可能会变"（PatchFlag）和"这些配料是固定的"（静态提升），这样厨师下次只需要重新调味，不用从头开始做。

> [!TIP]
>
> 回答时重点说**编译器做了什么优化**（PatchFlag、静态提升、Block Tree），不要只说"模板变成渲染函数'--想听的是你对为什么模板比手写 render 函数性能好的理解。

***

## Q2: v-model 的本质是什么？

v-model 是 Vue 中最常用的指令之一，但很多人不知道它在底层是怎么实现的。

```text theme={null}
v-model 的编译转换图解：

  模板层（你写的）          编译器转换           运行时（编译后）
  ┌──────────────┐      ┌────────────┐      ┌────────────────────┐
  │ <input        │      │            │      │ h('input', {       │
  │   v-model=    │  →   │  编译器拆分  │  →   │   value: name,     │ ← 数据→视图
  │   "name" />   │      │  双向绑定    │      │   onInput: e => {  │ ← 视图→数据
  └──────────────┘      └────────────┘      │     name = e.target │
                                             │       .value        │
                                             │   }                 │
                                             │ })                  │
                                             └────────────────────┘

  组件层（你写的）          编译器转换           运行时（编译后）
  ┌──────────────┐      ┌────────────┐      ┌────────────────────┐
  │ <MyInput      │      │            │      │ h(MyInput, {       │
  │   v-model=    │  →   │  编译器拆分  │  →   │   modelValue: name,│ ← prop
  │   "name" />   │      │  prop+事件   │      │   onUpdate:val => {│ ← 事件
  └──────────────┘      └────────────┘      │     name = val      │
                                             │   }                 │
                                             │ })                  │
                                             └────────────────────┘

  多 v-model（你写的）     编译器转换           运行时（编译后）
  ┌──────────────┐      ┌────────────┐      ┌────────────────────┐
  │ <UserForm     │      │            │      │ h(UserForm, {      │
  │  v-model:name │  →   │ 每个 model  │  →   │   name: name,      │
  │  v-model:age  │      │ 独立 prop+  │      │   onUpdate:name,   │
  │ />            │      │ 独立事件    │      │   age: age,        │
  └──────────────┘      └────────────┘      │   onUpdate:age,    │
                                             │ })                  │
                                             └────────────────────┘
```

### 原生 HTML 元素的 v-model

v-model 在原生元素上的本质是 **`value` + `input` 事件**的语法糖：

```javascript theme={null}
// 你写的：
<input v-model="name" />;

// 编译器转换后（简化）：
h("input", {
  value: name,
  onInput: (e) => {
    name = e.target.value;
  },
});
```

### 组件上的 v-model

v-model 在组件上的本质是 **`modelValue` prop + `update:modelValue` 事件**的语法糖：

```javascript theme={null}
// 你写的：
<MyInput v-model="name" />;

// 编译器转换后（简化）：
h(MyInput, {
  modelValue: name,
  "onUpdate:modelValue": (val) => {
    name = val;
  },
});

// 子组件内部：
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
```

Vue 3 还支持多个 v-model：

```javascript theme={null}
// 你写的：
<UserForm v-model:name="name" v-model:age="age" />;

// 编译器转换后：
h(UserForm, {
  name: name,
  "onUpdate:name": (val) => {
    name = val;
  },
  age: age,
  "onUpdate:age": (val) => {
    age = val;
  },
});
```

> [!WARNING]
>
> Vue 2 中，`.sync` 修饰符和 `v-model` 都是双向绑定的语法糖，但它们的编译产物不同：
>
> * `v-model`：`value` + `input`（原生）或 `modelValue` + `update:modelValue`（组件）
> * `.sync`：Vue 2 的 `prop` + `update:prop` 语法，Vue 3 已移除，改用多个 `v-model` 替代

> [!TIP]
>
> v-model 的本质是"两条独立线路的语法糖"--编译器把它拆成 `value` prop（数据→视图）和 `input` 事件（视图→数据）。
> 就像一个对讲机。你对着麦克风说话（输入框输入），对方的喇叭立刻播放你的声音（数据更新）。但实际上对讲机内部是两条独立的线路：一条从你到对方（`value` prop），一条从对方到你（`input` 事件）。v-model 只是帮你把这两条线路打包成了一个按钮。

> [!NOTE]
>
> v-model 是编译器语法糖——原生元素编译为 `value` + `input` 事件，组件编译为 `modelValue` + `update:modelValue` 事件。

***

## Q3: nextTick 的原理？为什么能拿到更新后的 DOM？

### 基础梳理

| 对比维度 | Vue 2 | Vue 3 |
| - | - | - |
| **实现方式** | 异步任务（Promise/MutationObserver，降级 setImmediate/setTimeout） | 微任务（Promise） |
| **返回值** | Promise（如果传入回调则无返回值） | 总是返回 Promise |
| **使用方式** | `this.$nextTick(cb)` 或 `Vue.nextTick(cb)` | `await nextTick()` |
| **执行时机** | DOM 更新完成后 | DOM 更新完成后 |

### 专家视角深度剖析

### 为什么需要 nextTick

很多人知道"nextTick 能拿到更新后的 DOM"，但说不清**为什么**。

Vue 的数据更新是**异步批量**的--你修改 `this.count = 1` 时，Vue 不会立刻更新 DOM，而是把更新任务放入队列（`queueJob`），等同步代码全部执行完后，在微任务中统一刷新队列、更新 DOM。

### nextTick 的底层实现

`nextTick` 的原理是：它也用 `Promise.resolve().then()` 创建一个微任务，但这个微任务在队列刷新**之后**执行。所以你在 `nextTick` 回调中访问 DOM，拿到的一定是更新后的状态。

```javascript theme={null}
// 源码简化
const queue = [];
let isFlushing = false;
let currentFlushPromise = null;
const resolvedPromise = Promise.resolve();

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job);
    queueFlush();
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true;
    currentFlushPromise = resolvedPromise.then(flushJobs); // 微任务中刷新队列
  }
}

function nextTick(fn) {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(fn) : p; // 排在 flushJobs 之后执行
}
```

```text theme={null}
同步代码执行：
  this.count = 1 → queueJob(update)
  this.name = 'new' → queueJob(update)（已存在，跳过）
  nextTick(() => { /* DOM 已更新 */ })

微任务队列：
  1. flushJobs() → 执行 queue 中的 update → DOM 更新
  2. nextTick 的回调 → 此时 DOM 已经是最新状态
```

> [!WARNING]
>
> 微任务在当前同步代码执行完毕后、浏览器渲染之前执行。这意味着：
>
> * DOM 更新发生在渲染之前，用户看到的永远是最新状态。
> * 如果用宏任务（setTimeout），DOM 更新会推迟到浏览器渲染之后，用户可能先看到旧状态，再看到新状态（闪烁）。

> [!TIP]
>
> nextTick 的本质是"等批量更新完成后再执行"--Vue 把多个数据修改合并成一次 DOM 更新，nextTick 的微任务在更新完成后执行。
> 就像快递代收点。你下了好几个单（修改多个响应式数据），快递不会每到一个就敲你家门（不会每次数据变化都更新 DOM）。它们先统一放到代收点（队列）。等所有快递都到了（同步代码执行完），代收点一次性通知你去取（微任务中刷新队列、更新 DOM）。nextTick 就是"等代收点通知完之后再执行"--你去取的时候，快递肯定已经到了。

***

## Q4: `[script setup]` 到底做了什么？

`[script setup]` 是 `setup()` 函数的语法糖。编译器会自动做以下事情：

1. **自动暴露**：顶层变量（ref、reactive、函数）自动暴露给模板，不需要手动 return
2. **自动导入**：编译器会把 import 的组件自动注册，不需要 components 选项
3. **编译为 setup 函数**：`[script setup]` 的代码被编译为 `setup()` 函数的函数体

```vue theme={null}
<!-- 你写的 -->
<script setup>
import { ref } from "vue";
import MyButton from "./MyButton.vue";

const count = ref(0);
function increment() {
  count.value++;
}
</script>
```

```javascript theme={null}
// 编译器转换后（简化）
import { ref } from "vue";
import MyButton from "./MyButton.vue";

export default {
  setup() {
    const count = ref(0);
    function increment() {
      count.value++;
    }
    return { count, increment, MyButton };
  },
  components: { MyButton },
};
```

> [!WARNING]
>
> `[script setup]` 中不能使用以下选项：`name`、`inheritAttrs`、`props`（用 `defineProps`）、`emits`（用 `defineEmits`）。Vue 3.3+ 新增了 `defineOptions` 宏，可以在 `[script setup]` 中声明 `name`、`inheritAttrs` 等选项。如果需要更多自定义选项，也可以用两个 `[script]` 块--一个普通 `[script]` 定义选项，一个 `[script setup]` 定义逻辑。

### script setup 到底做了什么？（编译时转换详解）

`[script setup]` 不是运行时 API，而是**编译时语法糖**。它在编译阶段被 Vue 的编译器转换为标准的 `setup()` 函数，同时做了四件事：

```text theme={null}
[script setup] 的编译时转换：

你写的：
  [script setup]
  import { ref } from 'vue'
  import MyButton from './MyButton.vue'
  const count = ref(0)
  function increment() { count.value++ }
  [/script]

编译器转换后（简化）：
  [script]
  import { ref } from 'vue'
  import MyButton from './MyButton.vue'
  export default {
    setup() {
      const count = ref(0)
      function increment() { count.value++ }
      return { count, increment, MyButton }
    },
    components: { MyButton }
  }
  [/script]
```

**编译器自动做的四件事：**

1. **自动暴露**：顶层变量（ref、reactive、函数）自动暴露给模板，编译器生成 `return { ... }` 语句
2. **自动注册组件**：import 的 `.vue` 文件自动注册为子组件，编译器生成 `components: { MyButton }`
3. **自动注册指令**：import 的自定义指令自动注册，编译器生成 `directives: { myDirective }`
4. **defineProps/defineEmits 编译时处理**：`defineProps` 和 `defineEmits` 不是运行时函数，而是编译器宏——编译器在编译时把它们转换为组件选项，运行时不存在

```javascript theme={null}
// defineProps 和 defineEmits 是编译器宏，不是运行时 API
// 你写的：
const props = defineProps({ count: Number });
const emit = defineEmits(["increment"]);

// 编译器转换后（简化）：
export default {
  props: { count: Number },
  emits: ["increment"],
  setup(props, { emit }) {
    // props 和 emit 已经作为参数传入
  },
};
```

> [!WARNING]
>
> `[script setup]` 相比普通 `setup()` 有三个性能优势：
>
> 1. **更好的 Tree Shaking**：编译器在编译时就知道哪些 import 被模板使用了，未使用的 import 可以被 Tree Shaking 删除
> 2. **更精确的类型推断**：TypeScript 可以直接分析 `[script setup]` 中的类型，不需要通过 `defineComponent` 包装
> 3. **更少的运行时开销**：自动暴露是编译时生成的 `return` 语句，运行时没有额外的代理或反射开销

> [!TIP]
>
> `[script setup]` 就像一个智能代码生成器。你只需要写核心逻辑（ref、函数、import），它自动帮你生成样板代码（return、components、directives）。就像你填一个表单的几个关键字段，系统自动生成完整的合同——你不用手动填写每个条款，但最终合同是完整的。

> [!NOTE]
>
> `[script setup]` 是 `setup()` 的编译时语法糖——编译器自动暴露顶层变量、注册导入组件、处理 defineProps/defineEmits 编译器宏。

***

## Q5: 模板为什么比 render 函数性能好？

### 编译器的三层优化

1. **PatchFlag**：编译器标记每个元素有哪些动态绑定（文本、class、style、props），运行时用位运算检查，只比较标记的部分。
2. **Block Tree**：扁平化动态后代数组，静态子树完全跳过。
3. **静态提升**：不依赖响应式的节点提升到渲染函数外部，只创建一次。

### render 函数为什么没有这些优化

render 函数是运行时执行的，编译器无法分析它的代码结构。编译器不知道哪些是静态节点、哪些是动态节点，所以只能全量 diff。

```text theme={null}
PatchFlag 位掩码的图解：

  模板: <div :class="cls" :style="sty">{{ msg }}</div>

  编译器分析：
  ├── :class="cls"  → CLASS (0b010 = 2)
  ├── :style="sty"  → STYLE (0b100 = 4)
  └── {{ msg }}     → TEXT  (0b001 = 1)
                      ─────────────────
  PatchFlag = 2 | 4 | 1 = 7 (0b111)

  运行时 diff：
  ┌─────────────────────────────────────────────────────┐
  │ patchFlag = 7 (0b111)                               │
  │                                                     │
  │ 7 & TEXT  (7 & 1) = 1  → ✅ 需要更新文本             │
  │ 7 & CLASS (7 & 2) = 2  → ✅ 需要更新 class           │
  │ 7 & STYLE (7 & 4) = 4  → ✅ 需要更新 style           │
  │ 7 & PROPS (7 & 8) = 0  → ❌ 跳过，没有 props 变化     │
  └─────────────────────────────────────────────────────┘
  → 只做 3 次比较，而不是遍历所有属性！
```

```javascript theme={null}
// 模板编译后的代码（有优化）
createElementVNode("div", null, [
  _createElementVNode("span", null, "静态"), // 静态提升（_hoisted_1），只创建一次
  _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
  //                                                       ↑ PatchFlag
  // 运行时只比较文本内容，跳过 class/style/props 等静态属性
]);

// render 函数（无优化，全量 diff）
h("div", [
  h("span", "静态"), // 每次都要比较这个节点的所有属性
  h("span", msg), // 每次都要比较这个节点的所有属性
]);
```

**关键区别：**

| 对比维度 | 模板 | render 函数 |
| - | - | - |
| **静态分析** | ✅ 编译器可以分析 | ❌ 无法分析 |
| **PatchFlag** | ✅ 有 | ❌ 没有 |
| **Block Tree** | ✅ 有 | ❌ 没有 |
| **静态提升** | ✅ 有 | ❌ 没有 |
| **灵活性** | 一般 | 高（可以写任意 JS 逻辑） |
| **适用场景** | 绝大多数场景 | 动态组件、高阶组件、库开发 |

> [!TIP]
>
> Vue 官方推荐用模板而不是手写 render 函数。如果你需要动态组件、高阶组件等复杂场景，可以用 render 函数，但要意识到你放弃了编译器的所有优化。

> [!TIP]
>
> render 函数就像手写作业——你必须一笔一画地描述每个字（手动创建 VNode），老师（浏览器）也不知道哪些字是重点、哪些是废话。模板就像交给印刷厂（编译器）——你只需要提供原稿（模板），印刷厂会自动帮你用红色标注重点（PatchFlag）、把不变的标题做成铅字模板（静态提升）、把动态内容单独排版（Block Tree），印刷效率远高于手抄。

### 编译时优化的本质：让编译器做尽可能多的工作

Vue 3 的设计理念是"让编译器做尽可能多的优化，减轻运行时的负担"。尤雨溪在 Vue 3 RFC 中明确表示：模板的"受限语法"不是缺点，而是优势——正是因为模板的结构是可预测的，编译器才能在编译时提取出优化信息。React 的 JSX 是任意 JavaScript 表达式，编译器无法静态分析哪些是动态部分，所以只能在运行时全量比较。

***

## 底层源码：模板编译器是怎么工作的？

模板编译的三个阶段是什么？

Vue 的模板编译器分三个阶段（详见 [Vue 3 源码 packages/compiler-core/src/compile.ts](https://github.com/vuejs/core/blob/main/packages/compiler-core/src/compile.ts)）：

```javascript theme={null}
// 编译器的入口函数
function baseCompile(template, options) {
  // 1. 解析：把模板字符串变成 AST（抽象语法树）
  const ast = parse(template, options);

  // 2. 转换：对 AST 做优化标记
  transform(ast, {
    ...options,
    nodeTransforms: [
      transformElement, // 处理元素节点
      transformText, // 处理文本节点
      transformExpression, // 处理表达式（如 {{ count }}）
      // ... 其他转换
    ],
  });

  // 3. 生成：把 AST 变成渲染函数代码
  return generate(ast, options);
}
```

这段代码做了三件事：

1. **parse（解析）**：把模板字符串 `<div>{{ count }}</div>` 解析成 AST 节点树。每个节点记录了标签名、属性、子节点等信息。

2. **transform（转换）**：遍历 AST，给节点打标记。比如给动态文本节点打上 `PatchFlag = TEXT`，把不依赖响应式的节点标记为"可提升"。

3. **generate（生成）**：把 AST 转换成 JavaScript 代码字符串，就是你看到的 `createElementVNode("div", null, ...)` 这种代码。

v-model 在编译器中是怎么处理的？

v-model 的编译实现在 `packages/compiler-core/src/transforms/vModel.ts` 中（详见 [Vue 3 源码 packages/compiler-core/src/transforms/vModel.ts](https://github.com/vuejs/core/blob/main/packages/compiler-core/src/transforms/vModel.ts)）。

编译器遇到 `v-model` 时，会根据元素类型生成不同的代码：

* **原生元素**（input/textarea/select）：生成 `value` prop + `onInput`/`onChange` 事件
* **组件**：生成 `modelValue` prop + `onUpdate:modelValue` 事件
* **带参数的 v-model**（如 `v-model:name`）：生成 `name` prop + `onUpdate:name` 事件

***

## 十、及格回答 vs 满分回答

### 问题："Vue 的模板是怎么编译的？"

**及格回答（60-70分）：**

> "模板经过编译器变成渲染函数，渲染函数返回虚拟 DOM，然后 diff 更新真实 DOM。"

问题出在：**知道流程但不理解优化**。

**满分回答（90-100分）：**

> Vue 的模板编译分三个阶段：**解析**（模板字符串 → AST）、**转换**（AST 优化标记）、**生成**（AST → 渲染函数代码）。编译器在转换阶段做了三层优化，这是模板比手写 render 函数性能好的根本原因。<sup>\[1]</sup>

> **PatchFlag**：编译器在 `createElementVNode` 调用中附加位掩码数字，标记元素有哪些动态绑定（TEXT=1、CLASS=2、STYLE=4、PROPS=8）。运行时用位运算检查，只比较标记的部分，跳过静态属性。React 没有这个--每次 diff 要比较所有属性。<sup>\[2]</sup>

> **Block Tree**：编译器把模板中结构稳定的部分（没有 v-if/v-for）标记为 Block，内部维护一个扁平化的动态后代数组。重渲染时只遍历这个数组中的动态节点，静态子树完全跳过。v-if/v-for 会创建新的子 Block，破坏父 Block 的稳定性。<sup>\[3]</sup>

> **静态提升**：不依赖响应式数据的节点（如 `<div>静态文本</div>`）被提升到渲染函数外部，只创建一次。后续更新时直接复用，不需要 diff。这就是为什么 Vue 3 的模板编译后代码里经常出现 `_hoisted_1`、`_hoisted_2` 这种变量。

### 常见追问

> [accordion] v-model 的本质是什么?
>
> **答题要点：** 原生元素是 value + input 事件，组件是 modelValue + update:modelValue 事件。
>
> **高分回答：**
> "v-model 是双向绑定的语法糖。在原生元素上，它编译为 `value` prop + `onInput` 事件。在组件上，它编译为 `modelValue` prop + `onUpdate:modelValue` 事件。
>
> ```javascript theme={null}
>   // 你写的：
>   <input v-model="name" />
>   // 编译器转换后：
>   h('input', { value: name, onInput: e => name = e.target.value })
>
>   // 组件上：
>   <MyInput v-model="name" />
>   // 编译器转换后：
>   h(MyInput, { modelValue: name, 'onUpdate:modelValue': val => name = val })
>
>   // 多个 v-model：
>   <UserForm v-model:name="name" v-model:age="age" />
>   // 编译器转换后：
>   h(UserForm, {
>     name: name, 'onUpdate:name': val => name = val,
>     age: age, 'onUpdate:age': val => age = val
>   })
> ```
>
> "

> [accordion] nextTick 的原理?为什么能拿到更新后的 DOM?
>
> **答题要点：** Vue 的数据更新是异步批量的，nextTick 的微任务在队列刷新之后执行。
>
> **高分回答：**
> "Vue 的数据更新是异步批量的--修改响应式数据后，更新任务被放入队列（`queueJob`），等同步代码执行完后在微任务中统一刷新。`nextTick` 也用 `Promise.resolve().then()` 创建微任务，但它在队列刷新之后执行，所以能拿到更新后的 DOM。
>
> ```javascript theme={null}
> // 源码简化（packages/runtime-core/src/scheduler.ts）
> const queue = [];
> const resolvedPromise = Promise.resolve();
>
> function queueJob(job) {
>   if (!queue.includes(job)) {
>     queue.push(job);
>     resolvedPromise.then(flushJobs); // 微任务中刷新
>   }
> }
>
> function nextTick(fn) {
>   return fn ? resolvedPromise.then(fn) : resolvedPromise;
>   // 在 flushJobs 之后执行，所以 DOM 已更新
> }
> ```
>
> "

> [accordion] 模板和 render 函数的性能差异?
>
> **答题要点：** 模板可以被编译器静态分析，render 函数不行。
>
> **高分回答：**
> "模板可以被编译器静态分析，生成的代码包含 PatchFlag（标记动态绑定）、Block Tree（扁平化动态节点）、静态提升（静态节点只创建一次）。render 函数是运行时执行的，编译器无法分析这些信息，只能全量 diff。所以 Vue 官方推荐用模板。"

> [accordion] 编译器的静态提升是怎么实现的?
>
> **答题要点：** 编译器在转换阶段识别不依赖响应式数据的节点，把它提升到渲染函数外部。
>
> **高分回答：**
> "编译器在转换阶段遍历 AST，检查每个节点是否依赖响应式数据。如果一个节点的所有子节点和属性都是静态的（没有绑定变量），编译器会把它从渲染函数中提取出来，赋值给一个外部变量（如 `_hoisted_1`）。这样每次执行渲染函数时，这个节点只被引用，不被重新创建。Vue 3 的源码中，这个优化在 `hoistStatic` 转换中实现。"

> [accordion] \[script setup] 和普通 setup() 函数有什么区别?
>
> **答题要点：** script setup 是编译时语法糖，自动暴露顶层变量和导入的组件。
>
> **高分回答：**
> "`[script setup]` 是 `setup()` 函数的编译时语法糖，编译器会自动做三件事：
>
> 1. **自动暴露**：顶层变量（ref、reactive、函数）自动暴露给模板，不需要手动 return。
> 2. **自动注册组件**：import 的组件自动注册，不需要 components 选项。
> 3. **编译为 setup 函数**：代码被编译为 `setup()` 的函数体。
>
> 性能优势：`[script setup]` 编译后的代码有更好的 tree-shaking 和类型推断。因为编译器在编译时就知道哪些变量被模板使用了，可以精确地生成代码。普通 `setup()` 是运行时函数，编译器无法静态分析。"

> [accordion] v-show 和 v-if 在编译器中有什么区别?
>
> **答题要点：** v-if 编译为条件 Block，v-show 编译为带 display 样式的节点。
>
> **高分回答：**
> "`v-if` 在编译器中被转换为三元表达式：`v-if="cond"` 编译为 `cond ? createVNode(...) : createCommentVNode(...)`。当条件为 false 时，节点不会被创建（卸载），这会影响 Block Tree 的结构——`v-if` 的每个分支都是一个独立的 Block。
>
> `v-show` 在编译器中不改变节点结构，只在运行时通过 `transitionHooks` 切换 `display: none`。节点始终存在于 DOM 中，不影响 Block Tree。
>
> 选型建议：频繁切换用 `v-show`（只改 CSS，不触发组件卸载/重建），条件很少改变用 `v-if`（不渲染的节点不占 DOM 空间）。"

> [accordion] 编译器是怎么处理 slot 的?
>
> **答题要点：** 具名 slot 编译为函数，作用域 slot 通过参数传递数据。
>
> **高分回答：**
> "Vue 3 的 slot 编译为函数。具名 slot `<slot name="header">` 在父组件中编译为 `h(Comp, { header: () => h('div', 'content') })`。子组件通过 `renderSlot($slots, 'header')` 调用这个函数来渲染。
>
> 作用域 slot `<slot :data="items">` 编译时会在 slot 函数中传入参数：`header: (data) => h('div', data)`。父组件通过 `#header="{ data }"` 解构获取数据。
>
> 这种函数式设计让 slot 的内容是惰性渲染的——只有当子组件实际调用 `renderSlot` 时，slot 的内容才会被渲染。如果子组件有条件地渲染 slot（`v-if` 控制），不满足条件时 slot 的 VNode 不会被创建。"

> [accordion] Vue 3 的模板编译和 Vue 2 有什么区别?
>
> **答题要点：** Vue 3 的编译器新增了 PatchFlag、Block Tree、静态提升等优化。
>
> **高分回答：**
> "Vue 2 的编译器只做基本的模板到 render 函数的转换，不附加任何优化信息。生成的 render 函数和手写的一样——每次渲染都遍历整棵 VNode 树。
>
> Vue 3 的编译器在转换阶段做了三层优化：
>
> 1. **PatchFlag**：给动态节点附加位掩码，告诉运行时哪些属性会变化。
> 2. **Block Tree**：把模板中结构稳定的区域标记为 Block，维护扁平化的动态后代数组。
> 3. **静态提升**：不依赖响应式的 VNode 提升到模块级别，只创建一次。
>
> 这些优化让 Vue 3 的模板编译后性能显著优于 Vue 2，甚至在特定场景下超越手写 render 函数——因为手写 render 无法享受编译器的静态分析。"

***

> [!NOTE]
>
> Composition API 的详细对比和 Composables 最佳实践等内容，详见 [Vue 生态与工程](https://opc-43d279b8.mintlify.site/vue/vue-ecosystem.md)。

***

## 延伸阅读与引用参考

* [Vue 3 编译器源码 packages/compiler-core](https://github.com/vuejs/core/tree/main/packages/compiler-core)：Vue 3 模板编译器的核心实现，包含 parse、transform、generate 三个阶段。<sup>\[1]</sup>
* [Vue 3 源码 packages/compiler-core/src/transforms/vModel.ts](https://github.com/vuejs/core/blob/main/packages/compiler-core/src/transforms/vModel.ts)：v-model 的编译实现。<sup>\[2]</sup>
* [Vue 3 源码 packages/runtime-core/src/scheduler.ts](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts)：nextTick 和 queueJob 的实现。<sup>\[3]</sup>
* [Vue 3 官方文档 - 渲染机制](https://vuejs.org/guide/extras/rendering-mechanism.html)：尤雨溪对模板编译和渲染机制的深度解析，包含 PatchFlag、Block Tree、静态提升的完整说明。
* [霍春阳《Vue.js 设计与实现》](https://book.douban.com/subject/35768338/)：从编译器 parse 到渲染器 patch 的完整源码剖析，包含模板编译三阶段的详细实现。
* [黄轶《Vue.js 技术揭秘》](https://ustbhuangyi.github.io/vue-analysis/)：从 Vue 2 编译器到 Vue 3 编译器的演进脉络，对 v-model 的编译转换有源码级分析。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.