摘要
文章详细阐述了 Vue2 模板编译里生成器将优化后的 AST 转换为渲染函数 JS 代码的过程,包括核心任务、入口方法、节点处理(以 v - if 和 v - for 为例)、子节点处理、静态节点处理等内容,还展示了整体架构流程。
1. 生成器概述
在 Vue2 模板编译中,生成器起着关键作用。它的主要任务是将优化后的抽象语法树(AST)转换为可执行的 JavaScript 代码,输出的是渲染函数,这个函数能够依据数据状态来创建或者更新 DOM 结构。例如对于模板<div v - if = "isShow"><li v - for = "item in items">{{item}}</li></div>
,编译后的渲染函数字符串为with(this) {return (isShow)? _c('div', _l((items), function (item) {return _c('li', [_v(_s(item))])}), 0) : _e()}
,这里的_c
、_l
、_v
、_s
、_e
等方法是 Vue 运行时的内置方法,属于运行时部分内容,不在本文重点讨论范围。
2. generate 入口方法
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
// 1. 创建 CodegenState 实例
const state = new CodegenState(options);
// 2. 根据 ast 情况确定 code
const code = ast
? ast.tag === 'script'
? 'null'
: genElement(ast, state)
: '_c("div")';
// 3. 返回包含渲染函数和静态渲染函数数组的对象
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
};
}
从这个入口方法可以看出,核心是通过genElement
方法把 AST 转化为code
字符串,最后利用with
语句包装后作为render
渲染函数字符串,同时返回对象中的staticRenderFns
用于保存静态渲染函数字符串数组。这里的CodegenState
实例可看作保存转换过程所需信息和状态的辅助类。
3. genElement 方法
export function genElement(el: ASTElement, state: CodegenState): string {
// 一系列 if - else 判断处理不同情况
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot &&!el.staticProcessed) {
return genStatic(el, state);
} else if (el.once &&!el.onceProcessed) {
return genOnce(el, state);
} else if (el.for &&!el.forProcessed) {
return genFor(el, state);
} else if (el.if &&!el.ifProcessed) {
return genIf(el, state);
} else if (el.tag === 'template' &&!el.slotTarget &&!state.pre) {
return genChildren(el, state) || 'void 0';
} else if (el.tag === 'slot') {
return genSlot(el, state);
} else {
let code;
let tag = `'${el.tag}'`;
const children = genChildren(el, state, true);
code = `_c(${tag}${
data? `,${data}` : ''
}${
children? `,${children}` : ''
})`;
// 模块转换操作
for (let i = 0; i < state.transforms.length; i ++) {
code = state.transforms[i](el, code);
}
return code;
}
}
这个方法首先进行很多if - else
判断,遇到对应的内置指令后会进入专门分支处理。指令分支较多,文章以v - if
和v - for
指令为例来探索渲染函数的生成过程。判断之后针对子节点会调用genChildren
方法生成子节点代码,最后返回根据_c(..)
形式拼接而成的渲染函数字符串。
4. v - if 处理
对于模板中的v - if
指令,如<div v - if = "isShow">//..</div>
,在genElement
方法中会进入else if (el.if &&!el.ifProcessed)
这个分支,然后调用genIf
函数。
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 1. 修改递归状态
el.ifProcessed = true;
// 2. 调用 genIfConditions 方法生成代码
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}
在genIf
函数中修改了递归状态后调用genIfConditions
方法,传入的el.ifConditions.slice()
是节点中的条件指令数组浅拷贝,避免后续处理影响原节点属性。genIfConditions
方法根据条件数组情况生成相应代码,如果条件数组为空返回备用空节点字符串或者默认空节点_e()
;如果条件不为空则返回三元表达式,三元表达式的真假值部分会调用genTernaryExp
方法,而genTernaryExp
方法最终会调用genElement
方法,由于前面设置了el.ifProcessed = true
,所以不会再次进入genIf
函数导致死循环。
5. v - for 处理
export function genFor(
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for;
const alias = el.alias;
const iterator1 = el.iterator1? `,${el.iterator1}` : '';
const iterator2 = el.iterator2? `,${el.iterator2}` : '';
// 设置递归状态
el.forProcessed = true;
return (
`${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
);
}
v - for
逻辑相对v - if
简单些。以<div v - for = "(value, name, index) in object"></div>
为例,结合其转换后的 AST 节点来看genFor
方法中的逻辑,先设置el.forProcessed = true
避免递归时再次进入,然后按照逻辑生成相应代码。
6. genChildren 方法
export function genChildren(
el: ASTElement,
state: CodegenState
): string | void {
const children = el.children;
if (children.length) {
const el: any = children[0];
// 特殊情况处理
if (
children.length === 1 &&
el.for &&
el.tag!== 'template' &&
el.tag!== 'slot'
) {
return `${(genElement)(el, state)}`;
}
// 循环生成子节点代码
return `[${children.map(c => genNode(c, state)).join(',')}]`;
}
}
genChildren
方法在genElement
方法中被调用来生成子节点代码。如果children
数组不为空,会根据不同情况处理,最后对子节点循环调用genNode
方法。genNode
方法根据 AST 节点类型分别调用对应方法生成具体代码,如genElement
(元素节点)、genComment
(注释节点)、genText
(文本节点且非注释节点)。
7. 静态节点处理
对于优化器标记为静态根节点(staticRoot: true
)的 AST 节点,会经过genStatic
方法处理。
function genStatic(el: ASTElement, state: CodegenState): string {
// 设置递归状态
el.staticProcessed = true;
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' : ''})`;
}
genStatic
方法核心逻辑主要两步:一是将genElement
产生的代码包装后保存到staticRenderFns
数组,表示静态根节点对应的渲染函数代码;二是最终返回形如_m(0)
的代码,参数是当前静态根节点在staticRenderFns
数组中的索引下标。
8. 架构流程
文章最后将生成器源码整体串起来展示了架构流程,通过解析器、优化器和生成器这三个核心步骤,深入了解了 Vue2 如何将模板转换为高效的渲染函数。