跳到主要内容

模板编译原理

问题

Vue 的模板是如何编译的?编译过程有哪些阶段?Vue 3 做了哪些编译优化?

答案

Vue 的模板编译是将模板字符串转换为渲染函数的过程。编译发生在构建时(SFC + 构建工具)或运行时(完整版 Vue)。

编译流程概览

三大阶段

阶段输入输出作用
Parse模板字符串AST词法分析 + 语法分析
TransformAST优化后的 AST静态分析 + 优化
GenerateASTrender 函数代码生成

1. Parse 阶段 - 解析

将模板字符串解析为抽象语法树(AST)

<!-- 模板 -->
<div id="app">
<span>{{ message }}</span>
</div>
// 解析后的 AST(简化)
const ast = {
type: 'Root',
children: [{
type: 'Element',
tag: 'div',
props: [{
type: 'Attribute',
name: 'id',
value: 'app'
}],
children: [{
type: 'Element',
tag: 'span',
children: [{
type: 'Interpolation',
content: {
type: 'Expression',
content: 'message'
}
}]
}]
}]
};

解析过程

// 简化的解析器
function parse(template: string): AST {
const context = {
source: template,
advance(num: number) {
this.source = this.source.slice(num);
}
};

const root: AST = {
type: 'Root',
children: parseChildren(context)
};

return root;
}

function parseChildren(context: Context): ASTNode[] {
const nodes: ASTNode[] = [];

while (!isEnd(context)) {
let node: ASTNode;

if (context.source.startsWith('{{')) {
// 解析插值 {{ }}
node = parseInterpolation(context);
} else if (context.source.startsWith('<')) {
// 解析元素 <div>
node = parseElement(context);
} else {
// 解析文本
node = parseText(context);
}

nodes.push(node);
}

return nodes;
}

2. Transform 阶段 - 转换与优化

对 AST 进行静态分析和优化:

function transform(ast: AST) {
const context = {
currentNode: null,
nodeTransforms: [
transformElement,
transformText,
transformInterpolation
]
};

traverseNode(ast, context);

// 添加 codegenNode(用于生成代码)
// 标记静态节点
// 提升静态内容
}

Vue 3 编译优化

PatchFlags(补丁标记)
<template>
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
<span :class="cls">动态 class</span>
<span :id="id" :class="cls">多个动态属性</span>
</div>
</template>

编译后:

import { createVNode as _createVNode, toDisplayString as _toDisplayString } from 'vue';

// PatchFlags 枚举
const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 2, // 动态 class
STYLE = 4, // 动态 style
PROPS = 8, // 动态 props(非 class/style)
FULL_PROPS = 16, // 有动态 key 的 props
// ...
}

function render() {
return _createVNode('div', null, [
// 静态节点:无 PatchFlag
_createVNode('span', null, '静态文本'),

// 动态文本:PatchFlag = 1 (TEXT)
_createVNode('span', null, _toDisplayString(dynamic), 1 /* TEXT */),

// 动态 class:PatchFlag = 2 (CLASS)
_createVNode('span', { class: cls }, '动态 class', 2 /* CLASS */),

// 多个动态属性:PatchFlag = 8 (PROPS) + 动态 key 数组
_createVNode('span', { id: id, class: cls }, '多个动态属性', 8 /* PROPS */, ['id', 'class'])
]);
}

Diff 时只检查有 PatchFlag 的节点,大幅提升性能。

静态提升(hoistStatic)
<template>
<div>
<span class="static">静态内容</span>
<span>{{ dynamic }}</span>
</div>
</template>

编译后:

// 静态节点被提升到渲染函数外部
const _hoisted_1 = _createVNode('span', { class: 'static' }, '静态内容');

function render() {
return _createVNode('div', null, [
_hoisted_1, // 复用,不重新创建
_createVNode('span', null, _toDisplayString(dynamic), 1 /* TEXT */)
]);
}
缓存事件处理函数
<template>
<button @click="handleClick">Click</button>
</template>

编译后:

function render(_ctx, _cache) {
return _createVNode('button', {
// 事件处理函数被缓存
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, 'Click');
}
Block Tree
<template>
<div><!-- Block -->
<span>静态</span>
<span>{{ dynamic1 }}</span>
<div v-if="show"><!-- Block -->
<span>{{ dynamic2 }}</span>
</div>
</div>
</template>

编译后,动态节点被收集到 Block 中:

function render() {
return (_openBlock(), _createBlock('div', null, [
_createVNode('span', null, '静态'),
_createVNode('span', null, _toDisplayString(dynamic1), 1 /* TEXT */),
show
? (_openBlock(), _createBlock('div', { key: 0 }, [
_createVNode('span', null, _toDisplayString(dynamic2), 1 /* TEXT */)
]))
: _createCommentVNode('v-if')
]));
// Block 的 dynamicChildren 只包含动态节点
}

Diff 时只需对比 dynamicChildren,跳过静态节点。

3. Generate 阶段 - 代码生成

将 AST 转换为渲染函数代码:

function generate(ast: AST): string {
const context = {
code: '',
push(code: string) {
this.code += code;
}
};

// 生成代码
context.push('function render() {');
context.push(' return ');
genNode(ast.children[0], context);
context.push('}');

return context.code;
}

function genNode(node: ASTNode, context: Context) {
switch (node.type) {
case 'Element':
genElement(node, context);
break;
case 'Text':
genText(node, context);
break;
case 'Interpolation':
genInterpolation(node, context);
break;
}
}

完整编译示例

<!-- 源模板 -->
<template>
<div id="app" class="container">
<h1>{{ title }}</h1>
<p v-if="show">Hello</p>
<button @click="handleClick">Click</button>
</div>
</template>
// 编译结果
import {
createVNode as _createVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createBlock as _createBlock,
createCommentVNode as _createCommentVNode
} from 'vue';

// 静态提升
const _hoisted_1 = { id: 'app', class: 'container' };

export function render(_ctx, _cache) {
return (_openBlock(), _createBlock('div', _hoisted_1, [
_createVNode('h1', null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_ctx.show
? (_openBlock(), _createBlock('p', { key: 0 }, 'Hello'))
: _createCommentVNode('v-if', true),
_createVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, 'Click')
]));
}

常见面试问题

Q1: Vue 的编译发生在什么时候?

答案

场景编译时机说明
SFC + 构建工具构建时vite/webpack 插件处理
运行时模板运行时需要完整版 Vue
JSX/TSX构建时Babel/esbuild 处理
// 构建时编译(推荐)
// .vue 文件在构建时被编译,产物只包含 render 函数

// 运行时编译(需要完整版 Vue)
import { createApp } from 'vue'; // 需要 'vue/dist/vue.esm-bundler.js'

createApp({
template: '<div>{{ msg }}</div>', // 运行时编译
data: () => ({ msg: 'Hello' })
});

Q2: Vue 3 编译优化有哪些?

答案

优化说明效果
PatchFlags标记动态内容类型Diff 时精确更新
静态提升静态节点只创建一次减少内存分配
事件缓存缓存事件处理函数避免子组件重渲染
Block Tree收集动态节点Diff 跳过静态节点
预字符串化连续静态节点合并为字符串减少 VNode 数量

Q3: 为什么 Vue 要使用虚拟 DOM 而不是直接编译成命令式 DOM 操作?

答案

// 直接编译成命令式操作
// 理论上更快,但有问题:
function render() {
const div = document.createElement('div');
div.textContent = this.msg; // 每次都重新设置
}

// 问题:
// 1. 难以处理条件渲染和列表渲染的复杂情况
// 2. 难以跨平台(SSR、小程序)
// 3. 更新时难以最小化 DOM 操作

虚拟 DOM 的优势:

  1. 声明式编程:描述结果,框架处理更新
  2. 跨平台:可渲染到不同目标
  3. 可预测:通过 Diff 确保最小化更新
  4. Vue 3 的平衡:编译时优化 + 运行时高效

Q4: 什么是 Block Tree?

答案

Block Tree 是 Vue 3 的优化策略:

  1. Block:一个稳定结构的节点(无 v-if/v-for 的区域)
  2. dynamicChildren:Block 内的动态节点数组
  3. Diff 优化:只对比 dynamicChildren,跳过静态子节点
// 普通 VNode Tree:需要完整遍历
// Block Tree:只遍历动态节点

// 示例:100 个静态节点 + 1 个动态节点
// 普通 Diff:101 次比较
// Block Diff:1 次比较

Q5: 如何查看 Vue 组件编译后的代码?

答案

  1. Vue SFC Playgroundhttps://play.vuejs.org
  2. Vite 查看:开发模式下看 Network 面板
  3. 构建产物:查看 dist 目录
  4. Vue 编译器 API
import { compile } from '@vue/compiler-dom';

const { code } = compile(`
<div>
<span>{{ msg }}</span>
</div>
`);

console.log(code);

相关链接