- codegen
- generate
- genIf
- genFor
- genData & genChildren
- 总结
codegen
编译的最后一步就是把优化后的 AST 树转换成可执行的代码,这部分内容也比较多,我并不打算把所有的细节都讲了,了解整体流程即可。部分细节我们会在之后的章节配合一个具体 case 去详细讲。
为了方便理解,我们还是用之前的例子:
<ul :class="bindCls" class="list" v-if="isShow"><li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li></ul>
它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:
with(this){return (isShow) ?_c('ul', {staticClass: "list",class: bindCls},_l((data), function(item, index) {return _c('li', {on: {"click": function($event) {clickItem(index)}}},[_v(_s(item) + ":" + _s(index))])})) : _e()}
这里的 _c 函数定义在 src/core/instance/render.js 中。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
而 _l、_v 定义在 src/core/instance/render-helpers/index.js 中:
export function installRenderHelpers (target: any) {target._o = markOncetarget._n = toNumbertarget._s = toStringtarget._l = renderListtarget._t = renderSlottarget._q = looseEqualtarget._i = looseIndexOftarget._m = renderStatictarget._f = resolveFiltertarget._k = checkKeyCodestarget._b = bindObjectPropstarget._v = createTextVNodetarget._e = createEmptyVNodetarget._u = resolveScopedSlotstarget._g = bindObjectListeners}
顾名思义,_c 就是执行 createElement 去创建 VNode,而 _l 对应 renderList 渲染列表;_v 对应 createTextVNode 创建文本 VNode;_e 对于 createEmptyVNode创建空的 VNode。
在 compileToFunctions 中,会把这个 render 代码串转换成函数,它的定义在 src/compler/to-function.js 中:
const compiled = compile(template, options)res.render = createFunction(compiled.render, fnGenErrors)function createFunction (code, errors) {try {return new Function(code)} catch (err) {errors.push({ err, code })return noop}}
实际上就是把 render 代码串通过 new Function 的方式转换成可执行的函数,赋值给 vm.options.render,这样当组件通过 vm._render 的时候,就会执行这个 render 函数。那么接下来我们就重点关注一下这个 render 代码串的生成过程。
generate
const code = generate(ast, options)
generate 函数的定义在 src/compiler/codegen/index.js 中:
export function generate (ast: ASTElement | void,options: CompilerOptions): CodegenResult {const state = new CodegenState(options)const code = ast ? genElement(ast, state) : '_c("div")'return {render: `with(this){return ${code}}`,staticRenderFns: state.staticRenderFns}}
generate 函数首先通过 genElement(ast, state) 生成 code,再把 code 用 with(this){return ${code}}} 包裹起来。这里的 state 是 CodegenState 的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 genElement:
export function genElement (el: ASTElement, state: CodegenState): string {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) {return genChildren(el, state) || 'void 0'} else if (el.tag === 'slot') {return genSlot(el, state)} else {// component or elementlet codeif (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}` : '' // data}${children ? `,${children}` : '' // children})`}// module transformsfor (let i = 0; i < state.transforms.length; i++) {code = state.transforms[i](el, code)}return code}}
基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,在我们的例子中,我们先了解一下 genFor 和 genIf。
genIf
export function genIf (el: any,state: CodegenState,altGen?: Function,altEmpty?: string): string {el.ifProcessed = true // avoid recursionreturn genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)}function genIfConditions (conditions: ASTIfConditions,state: CodegenState,altGen?: Function,altEmpty?: string): string {if (!conditions.length) {return altEmpty || '_e()'}const condition = conditions.shift()if (condition.exp) {return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions, state, altGen, altEmpty)}`} else {return `${genTernaryExp(condition.block)}`}// v-if with v-once should generate code like (a)?_m(0):_m(1)function genTernaryExp (el) {return altGen? altGen(el, state): el.once? genOnce(el, state): genElement(el, state)}}
genIf 主要是通过执行 genIfConditions,它是依次从 conditions 获取第一个 condition,然后通过对 condition.exp 去生成一段三元运算符的代码,: 后是递归调用 genIfConditions,这样如果有多个 conditions,就生成多层三元运算逻辑。这里我们暂时不考虑 v-once 的情况,所以genTernaryExp 最终是调用了 genElement。
在我们的例子中,只有一个 condition,exp 为 isShow,因此生成如下伪代码:
return (isShow) ? genElement(el, state) : _e()
genFor
export function genFor (el: any,state: CodegenState,altGen?: Function,altHelper?: string): string {const exp = el.forconst alias = el.aliasconst iterator1 = el.iterator1 ? `,${el.iterator1}` : ''const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''if (process.env.NODE_ENV !== 'production' &&state.maybeComponent(el) &&el.tag !== 'slot' &&el.tag !== 'template' &&!el.key) {state.warn(`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +`v-for should have explicit keys. ` +`See https://vuejs.org/guide/list.html#key for more info.`,true /* tip */)}el.forProcessed = true // avoid recursionreturn `${altHelper || '_l'}((${exp}),` +`function(${alias}${iterator1}${iterator2}){` +`return ${(altGen || genElement)(el, state)}` +'})'}
genFor 的逻辑很简单,首先 AST 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。
在我们的例子中,exp 是 data,alias 是 item,iterator1 ,因此生成如下伪代码:
_l((data), function(item, index) {return genElememt(el, state)})
genData & genChildren
再次回顾我们的例子,它的最外层是 ul,首先执行 genIf,它最终调用了 genElement(el, state) 去生成子节点,注意,这里的 el 仍然指向的是 ul 对应的 AST 节点,但是此时的 el.ifProcessed 为 true,所以命中最后一个 else 逻辑:
// component or elementlet codeif (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}` : '' // data}${children ? `,${children}` : '' // children})`}// module transformsfor (let i = 0; i < state.transforms.length; i++) {code = state.transforms[i](el, code)}return code
这里我们只关注 2 个逻辑,genData 和 genChildren:
- genData
export function genData (el: ASTElement, state: CodegenState): string {let data = '{'// directives first.// directives may mutate the el's other properties before they are generated.const dirs = genDirectives(el, state)if (dirs) data += dirs + ','// keyif (el.key) {data += `key:${el.key},`}// refif (el.ref) {data += `ref:${el.ref},`}if (el.refInFor) {data += `refInFor:true,`}// preif (el.pre) {data += `pre:true,`}// record original tag name for components using "is" attributeif (el.component) {data += `tag:"${el.tag}",`}// module data generation functionsfor (let i = 0; i < state.dataGenFns.length; i++) {data += state.dataGenFns[i](el)}// attributesif (el.attrs) {data += `attrs:{${genProps(el.attrs)}},`}// DOM propsif (el.props) {data += `domProps:{${genProps(el.props)}},`}// event handlersif (el.events) {data += `${genHandlers(el.events, false, state.warn)},`}if (el.nativeEvents) {data += `${genHandlers(el.nativeEvents, true, state.warn)},`}// slot target// only for non-scoped slotsif (el.slotTarget && !el.slotScope) {data += `slot:${el.slotTarget},`}// scoped slotsif (el.scopedSlots) {data += `${genScopedSlots(el.scopedSlots, state)},`}// component v-modelif (el.model) {data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`}// inline-templateif (el.inlineTemplate) {const inlineTemplate = genInlineTemplate(el, state)if (inlineTemplate) {data += `${inlineTemplate},`}}data = data.replace(/,$/, '') + '}'// v-bind data wrapif (el.wrapData) {data = el.wrapData(data)}// v-on data wrapif (el.wrapListeners) {data = el.wrapListeners(data)}return data}
genData 函数就是根据 AST 元素节点的属性构造出一个 data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。
之前我们提到了 CodegenState 的实例 state,这里有一段关于 state 的逻辑:
for (let i = 0; i < state.dataGenFns.length; i++) {data += state.dataGenFns[i](el)}
state.dataGenFns 的初始化在它的构造器中。
export class CodegenState {constructor (options: CompilerOptions) {// ...this.dataGenFns = pluckModuleFunction(options.modules, 'genData')// ...}}
实际上就是获取所有 modules 中的 genData 函数,其中,class module 和 style module 定义了 genData 函数。比如定义在 src/platforms/web/compiler/modules/class.js 中的 genData 方法:
function genData (el: ASTElement): string {let data = ''if (el.staticClass) {data += `staticClass:${el.staticClass},`}if (el.classBinding) {data += `class:${el.classBinding},`}return data}
在我们的例子中,ul AST 元素节点定义了 el.staticClass 和 el.classBinding,因此最终生成的 data 字符串如下:
{staticClass: "list",class: bindCls}
- genChildren
接下来我们再来看一下genChildren,它的定义在src/compiler/codegen/index.js中:
export function genChildren (el: ASTElement,state: CodegenState,checkSkip?: boolean,altGenElement?: Function,altGenNode?: Function): string | void {const children = el.childrenif (children.length) {const el: any = children[0]if (children.length === 1 &&el.for &&el.tag !== 'template' &&el.tag !== 'slot') {return (altGenElement || genElement)(el, state)}const normalizationType = checkSkip? getNormalizationType(children, state.maybeComponent): 0const gen = altGenNode || genNodereturn `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : ''}`}}
在我们的例子中,li AST 元素节点是 ul AST 元素节点的 children 之一,满足 (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot') 条件,因此通过 genElement(el, state) 生成 li AST元素节点的代码,也就回到了我们之前调用 genFor 生成的代码,把它们拼在一起生成的伪代码如下:
return (isShow) ?_c('ul', {staticClass: "list",class: bindCls},_l((data), function(item, index) {return genElememt(el, state)})) : _e()
在我们的例子中,在执行 genElememt(el, state) 的时候,el 还是 li AST 元素节点,el.forProcessed 已为 true,所以会继续执行 genData 和 genChildren 的逻辑。由于 el.events 不为空,在执行 genData 的时候,会执行 如下逻辑:
if (el.events) {data += `${genHandlers(el.events, false, state.warn)},`}
genHandlers 的定义在 src/compiler/codegen/events.js 中:
export function genHandlers (events: ASTElementHandlers,isNative: boolean,warn: Function): string {let res = isNative ? 'nativeOn:{' : 'on:{'for (const name in events) {res += `"${name}":${genHandler(name, events[name])},`}return res.slice(0, -1) + '}'}
genHandler 的逻辑就不介绍了,很大部分都是对修饰符 modifier 的处理,感兴趣同学可以自己看,对于我们的例子,它最终 genData 生成的 data 字符串如下:
{on: {"click": function($event) {clickItem(index)}}}
genChildren 的时候,会执行到如下逻辑:
export function genChildren (el: ASTElement,state: CodegenState,checkSkip?: boolean,altGenElement?: Function,altGenNode?: Function): string | void {// ...const normalizationType = checkSkip? getNormalizationType(children, state.maybeComponent): 0const gen = altGenNode || genNodereturn `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : ''}`}function genNode (node: ASTNode, state: CodegenState): string {if (node.type === 1) {return genElement(node, state)} if (node.type === 3 && node.isComment) {return genComment(node)} else {return genText(node)}}
genChildren 的就是遍历 children,然后执行 genNode 方法,根据不同的 type 执行具体的方法。在我们的例子中,li AST 元素节点的 children 是 type 为 2 的表达式 AST 元素节点,那么会执行到 genText(node) 逻辑。
export function genText (text: ASTText | ASTExpression): string {return `_v(${text.type === 2? text.expression: transformSpecialNewlines(JSON.stringify(text.text))})`}
因此在我们的例子中,genChildren 生成的代码串如下:
[_v(_s(item) + ":" + _s(index))]
和之前拼在一起,最终生成的 code 如下:
return (isShow) ?_c('ul', {staticClass: "list",class: bindCls},_l((data), function(item, index) {return _c('li', {on: {"click": function($event) {clickItem(index)}}},[_v(_s(item) + ":" + _s(index))])})) : _e()
总结
这一节通过例子配合解析,我们对从 ast -> code 这一步有了一些了解,编译后生成的代码就是在运行时执行的代码。由于 genCode 的内容有很多,所以我对大家的建议是没必要把所有的细节都一次性看完,我们应该根据具体一个 case,走完一条主线即可。
在之后的章节我们会对 slot 的实现做解析,我们会重新复习编译的章节,针对具体问题做具体分析,有利于我们排除干扰,对编译过程的学习有更深入的理解。
原文: https://ustbhuangyi.github.io/vue-analysis/compile/codegen.html
