版本 :以
vue@2.7.16代码为参考
概念理解
抽象语法树(AST)
抽象语法树:是源代码抽象结构表示,是模板语法和最终渲染代码的中间形态。通过抽象语法树可以完成代码压缩、代码转换、静态节点优化等操作。
AST 的作用:
- 代码转换:可以基于 AST 进行代码转换,如 Babel 兼容处理、代码压缩等
- 代码生成:递归遍历 AST 树,重新生成最终的代码结果,拼接成完整代码
- 静态分析:分析代码结构,进行静态节点优化、依赖分析等
AST 构建示例:
javascript
// 简化的 AST 构建示例
let ast = {
type: "Program",
body: [
{
type: "CallExpression",
name: "add",
params: [
{
type: "NumberLiteral",
value: token.value,
},
],
},
],
};
// walk 是一个递归执行的函数
function walk() {
let token = tokens[current];
// 数字类型语法树转换
if (token.type === "number") {
return {
type: "NumberLiteral",
value: token.value,
};
}
// 函数调用执行判断
if (token.type === "paren" && token.value === "(") {
let node = {
type: "CallExpression",
name: token.value,
params: [],
};
token = tokens[++current];
// 判断非结束括号时,递归执行,获取 params 参数
while (token.type !== "paren" || token.value !== ")") {
node.params.push(walk());
token = tokens[++current];
}
return node;
}
}
let current = 0;
while (current < tokens.length) {
ast.body.push(walk());
}
最终构建成一棵 AST 语法树。
Vue 2 模板编译流程
Vue 2 将 template 转成渲染函数的过程包含四个主要步骤:
- 模板解析 :调用
parseHTML把模板解析成抽象语法树,包括属性、事件、元素标签。 - 静态节点优化:标记抽象语法树中的静态节点,然后递归标记子节点,如果子节点不是静态节点,那么整个节点就标记非静态。
- 代码(字符串)生成 :把 AST 语法树转换为
render()函数,同时生成staticRenderFns用于静态节点渲染(生成均为代码字符串而非实际执行代码),后续组件渲染时执行 render 函数返回 VNode。 - 函数创建【后续步骤,可忽略】:把第 3 步生成的代码字符串转换为真实函数返回。
如果是使用构建框架,编译阶段会在
vue-loader插件内完成,这样可以减少运行时的编译流程如果是直接使用 编译 + 运行时的 Vue 框架,那编译阶段就会在组件初始化的时候完成:
created执行后,beforeMount执行前,Vue 会改写Vue.prototype.$mount函数,加入模板编译的代码流程,然后再执行组件挂载流程mountComponent()。
核心编译函数
typescript
// src/compiler/index.ts
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 解析模板,得到抽象语法树
const ast = parse(template.trim(), options);
// 2. 静态节点编译优化
if (options.optimize !== false) {
optimize(ast, options);
}
// 3. 构建 AST,生成渲染函数代码
const code = generate(ast, options);
return {
ast,
render: code.render, // 渲染函数
staticRenderFns: code.staticRenderFns, // 静态渲染函数
};
});
1. 模板解析(Parse)
模板解析需要处理以下节点类型:
- 文本节点:纯文本内容。
- 开始标签:HTML 元素的开始标签。
- 闭合标签:和开始标签分开处理。
- HTML 注释节点 :
<!-- 注释 -->。 - 条件注释 :
<!-- [if IE]> --> 内容 <!--< ![endif] -->。 - DOCTYPE 节点 :
<!DOCTYPE html>。
调用 parseHTML,传入模板和处理函数:
- start:创建 AST 元素节点,处理开始标签的 v-if、v-for、v-on 的动态绑定事件。
- end:结束标签处理。
- chars :创建 AST 文本节点,处理文本中的表达式节点。如果遇到文本节点,会调用
parseText处理文本节点,如果存在{``{ xxx | filter }}过滤器,就会调用parseFilter处理过滤器情况。 - comment:创建 AST 注释节点。
源码实现:
typescript
export function processFor(el: ASTElement) {
let exp;
if ((exp = getAndRemoveAttr(el, "v-for"))) {
// 解析 v-for
const res = parseFor(exp);
if (res) {
extend(el, res);
} else if (__DEV__) {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap["v-for"]);
}
}
}
// 解析 v-if 标签
function processIf(el) {
const exp = getAndRemoveAttr(el, "v-if");
if (exp) {
el.if = exp;
addIfCondition(el, {
exp: exp,
block: el,
});
} else {
if (getAndRemoveAttr(el, "v-else") != null) {
el.else = true;
}
const elseif = getAndRemoveAttr(el, "v-else-if");
if (elseif) {
el.elseif = elseif;
}
}
}
function makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> {
const map = {};
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value;
}
return map;
}
// AST 创建函数
export function createASTElement(
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: [],
};
}
// src/compiler/parser/index.ts
export function parse(
template: string,
options: CompilerOptions
): ASTElement | void {
// 解析 HTML,生成 AST 节点树
parseHTML(template, {
// 处理开始标签
start(tag, attrs, unary, start, end) {
// 创建 AST 元素节点
let element: ASTElement = createASTElement(tag, attrs, currentParent);
// 处理指令(v-if, v-for, v-on 等)
processFor(element);
processIf(element);
processOnce(element);
// ...
},
// 处理结束标签
end(tag, start, end) {
// 闭合标签,构建父子关系
},
// 处理文本
chars(text: string, start: number, end: number) {
// 创建文本节点或表达式节点
// {{ message }} → 表达式节点
},
// 处理注释
comment(text: string, start, end) {
// 创建注释节点
},
});
return root;
}
AST 节点类型
模板会被转换成一个个元素节点,分为元素节点、文本节点、表达式节点,不同节点类型用于优化数据结构,减少额外数据生成。
type标记节点类型。tag表示标签字段。children表示子节点 AST 树。
typescript
// 元素节点
interface ASTElement {
type: 1;
tag: string;
attrsList: Array<{ name: string; value: any }>;
attrsMap: { [key: string]: any };
parent: ASTElement | null;
children: Array<ASTNode>;
// 指令相关
if?: string;
ifConditions?: Array<ASTIfCondition>;
for?: string;
forProcessed?: boolean;
key?: string;
// 静态标记
static?: boolean;
staticRoot?: boolean;
// ...
}
// 文本节点
interface ASTText {
type: 3;
text: string;
static?: boolean;
}
// 表达式节点
interface ASTExpression {
type: 2;
expression: string;
text: string;
static?: boolean;
}
示例 :'<div>{``{ message }}</div>' 解析为:
typescript
{
type: 1,
tag: 'div',
children: [{
type: 2,
expression: '_s(message)',
text: '{{ message }}'
}]
}
2. 静态节点优化(Optimize)
静态节点优化通过标记静态节点来提升渲染性能:
- 如果是文本节点,标记为静态。
- 如果是带表达式的节点,则标记为非静态。
- 不包含 v-bind、v-if、v-for 等动态绑定标记,就标记为静态节点
node.static = true - 标记为静态节点并不代表节点就可以被编译优化,需要经过
markStaticRoots()将其标记为根静态节点node.staticRoot = true,其代码才会在后续的 "代码生成" 阶段被存入静态渲染函数列表staticRenderFns。
typescript
// src/compiler/optimizer.ts
export function optimize(
root: ASTElement | null | undefined,
options: CompilerOptions
) {
if (!root) return;
// 第一步:标记所有静态节点
markStatic(root);
// 第二步:标记静态根节点
markStaticRoots(root, false);
}
function markStatic(node: ASTNode) {
// 判断节点是否为静态
node.static = isStatic(node);
if (node.type === 1) {
// 元素节点:递归标记子节点
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i];
markStatic(child);
if (!child.static) {
node.static = false; // 子节点非静态,父节点也非静态
}
}
}
}
function markStaticRoots(node: ASTNode, isInFor: boolean) {
// 前提是元素节点
if (node.type === 1) {
// 设置for静态节点标记
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (
// 自身静态
node.static &&
// 有子节点
node.children.length &&
// 子节点不只一个,若为一个那该节点不能为文本节点
!(node.children.length === 1 && node.children[0].type === 3)
) {
// 认定为根静态节点
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
// 否则遍历子节点,标记对应的根静态节点
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor);
}
}
}
}
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// 表达式节点:非静态
return false;
}
if (node.type === 3) {
// 文本节点:静态
return true;
}
// 元素节点:检查是否有动态绑定
return !(
(
node.hasBindings || // 有绑定(v-bind, v-on 等)
node.if || // v-if
node.for || // v-for
isBuiltInTag(node.tag) || // 内置标签(slot, component)
isPlatformReservedTag(node.tag) === false || // 非平台保留标签
node.directives.length || // 有指令
node.staticInFor
) // 在 v-for 中
);
}
3. 代码(字符串)生成(Generate)
根据 AST 标识的节点类型,结合渲染辅助函数,生成最终渲染函数代码字符串:
- 根据节点属性 v-for、v-if、v-once、是否有静态标记、是否有组件,走不同的渲染函数生成流程。
- 通过
with修改作用域表达。 - 生成
render和staticRenderFns静态渲染函数代码字符串。
渲染辅助函数:
_c:createElement,创建 VNode。_v:createTextVNode,创建文本节点。_s:toString,转换为字符串。_l:renderList,渲染列表(v-for)。_m:renderStatic,渲染静态节点。_e:createEmptyVNode,创建空节点。
源码实现:
typescript
// src/compiler/codegen/index.ts
function genStatic(el: ASTElement, state: CodegenState): string {
el.staticProcessed = true;
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre;
if (el.pre) {
state.pre = el.pre;
}
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
state.pre = originalPreState;
return `_m(${state.staticRenderFns.length - 1}${
el.staticInFor ? ",true" : ""
})`;
}
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options);
// 生成代码
const code = ast
? ast.tag === "script"
? "null"
: genElement(ast, state) // 递归生成元素代码
: '_c("div")';
return {
render: `with(this){return ${code}}`, // 包装为函数体
staticRenderFns: state.staticRenderFns,
};
}
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) {
// 静态根节点
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
// v-once
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
// v-for
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
// v-if
return genIf(el, state);
} else if (el.tag === "slot") {
// slot
return genSlot(el, state);
} else {
// 普通元素或组件
let code;
if (el.component) {
// 组件
code = genComponent(el.component, el, state);
} else {
// 普通元素
const data = el.plain ? undefined : genData(el, state);
const children = el.inlineTemplate ? null : genChildren(el, state, true);
code = `_c('${el.tag}'${data ? `,${data}` : ""}${
children ? `,${children}` : ""
})`;
}
return code;
}
}
生成代码示例
javascript
// 模板
<div id="app" :class="className">
<p>{{ message }}</p>
<button @click="handleClick">Click</button>
</div>
// 生成的 render 函数代码
with(this) {
return _c('div', {
attrs: { "id": "app" },
class: className
}, [
_c('p', [_v(_s(message))]),
_c('button', {
on: { "click": handleClick }
}, [_v("Click")])
])
}
4. 函数创建
我觉得这一步其实才是实际代码生成阶段,创建将代码字符串编译成实际函数的编译方法
返回的编译函数会在传入的 VNode template 时,将代码字符串编译成实际的渲染函数并缓存
上述的 "代码生成" 阶段实际是代码字符串生成,没有转换成真正的函数
目前很多文章只是讲到 compile 函数完成时,没有讲最后的步骤
在完成 render 和 staticRenderFns 渲染函数代码字符串生成后。
在 createCompileToFunctionFn() 函数内会调用 compile() 完成模板编译,然后将返回的代码字符串转换为函数
之后使用 new Function() 将字符串转换为函数,编译结果会被缓存,相同模板只编译一次,再后续初始化时直接从缓存中获取渲染函数即可。
typescript
// src/compiler/to-function.ts
export function createCompileToFunctionFn(compile: Function): Function {
const cache = Object.create(null); // 缓存编译结果
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// 1. 检查缓存
const key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key];
}
// 2. 编译模板
const compiled = compile(template, options);
// 3. 将代码字符串转换为函数
const res: any = {};
const fnGenErrors: any[] = [];
// 把模板编译后返回的 render,转换成字符串返回
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map((code) => {
return createFunction(code, fnGenErrors);
});
// 4. 缓存结果
return (cache[key] = res);
};
}
function createFunction(code, errors) {
try {
return new Function(code); // 使用 Function 构造函数创建函数
} catch (err: any) {
errors.push({ err, code });
return noop;
}
}
整体流程
bash
Template: '<div>{{ message }}</div>'
↓
[Parse] 解析
↓
AST: { tag: 'div', children: [{ type: 2, expression: '_s(message)' }] }
↓
[Optimize] 优化
↓
AST: { tag: 'div', static: false, children: [{ static: false }] }
↓
[Generate] 生成代码
↓
Code: "with(this){return _c('div',[_v(_s(message))])}"
↓
[Create Function] 函数创建
↓
Render Function: function() { with(this){return _c('div',[_v(_s(message))])} }
思考
1. 为什么 Vue 2 使用 with 语句?
with 语句用于修改作用域,让 render 函数中的变量可以直接访问组件实例的属性,而不需要通过 this. 前缀。例如 _c('div') 可以直接访问,而不需要 this._c('div')。
注意 :with 语句在严格模式下不可用,且会影响性能优化,Vue 3 已经移除了 with 语句。
2. 静态节点优化为什么能提升性能?
静态节点在 patch 更新时可以直接跳过 diff 比较,因为静态节点的内容不会改变。静态根节点创建一次后,后续直接复用,避免了重复创建 VNode 和 DOM 操作的开销。
总结
-
核心流程:模板解析(Parse)→ 静态节点优化(Optimize)→ 代码生成(Generate)→ 函数创建。
- AST 节点类型:元素节点(type: 1)、表达式节点(type: 2)、文本节点(type: 3)。
- 静态节点优化:通过标记静态节点,在 patch 时跳过 diff,提升渲染性能。
- 代码生成 :根据 AST 节点类型和指令,使用对应的渲染辅助函数
_c、_v、_s、_l、_m、_e包裹。
-
优化点:
- 静态节点优化减少不必要的 diff 操作。
- 编译结果缓存避免重复编译。