摘要
文章详细阐述了 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 如何将模板转换为高效的渲染函数。