• codegen
    • generate
    • genIf
    • genFor
    • genData & genChildren
    • 总结

    codegen

    编译的最后一步就是把优化后的 AST 树转换成可执行的代码,这部分内容也比较多,我并不打算把所有的细节都讲了,了解整体流程即可。部分细节我们会在之后的章节配合一个具体 case 去详细讲。

    为了方便理解,我们还是用之前的例子:

    1. <ul :class="bindCls" class="list" v-if="isShow">
    2. <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
    3. </ul>

    它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:

    1. with(this){
    2. return (isShow) ?
    3. _c('ul', {
    4. staticClass: "list",
    5. class: bindCls
    6. },
    7. _l((data), function(item, index) {
    8. return _c('li', {
    9. on: {
    10. "click": function($event) {
    11. clickItem(index)
    12. }
    13. }
    14. },
    15. [_v(_s(item) + ":" + _s(index))])
    16. })
    17. ) : _e()
    18. }

    这里的 _c 函数定义在 src/core/instance/render.js 中。

    1. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

    _l_v 定义在 src/core/instance/render-helpers/index.js 中:

    1. export function installRenderHelpers (target: any) {
    2. target._o = markOnce
    3. target._n = toNumber
    4. target._s = toString
    5. target._l = renderList
    6. target._t = renderSlot
    7. target._q = looseEqual
    8. target._i = looseIndexOf
    9. target._m = renderStatic
    10. target._f = resolveFilter
    11. target._k = checkKeyCodes
    12. target._b = bindObjectProps
    13. target._v = createTextVNode
    14. target._e = createEmptyVNode
    15. target._u = resolveScopedSlots
    16. target._g = bindObjectListeners
    17. }

    顾名思义,_c 就是执行 createElement 去创建 VNode,而 _l 对应 renderList 渲染列表;_v 对应 createTextVNode 创建文本 VNode;_e 对于 createEmptyVNode创建空的 VNode。

    compileToFunctions 中,会把这个 render 代码串转换成函数,它的定义在 src/compler/to-function.js 中:

    1. const compiled = compile(template, options)
    2. res.render = createFunction(compiled.render, fnGenErrors)
    3. function createFunction (code, errors) {
    4. try {
    5. return new Function(code)
    6. } catch (err) {
    7. errors.push({ err, code })
    8. return noop
    9. }
    10. }

    实际上就是把 render 代码串通过 new Function 的方式转换成可执行的函数,赋值给 vm.options.render,这样当组件通过 vm._render 的时候,就会执行这个 render 函数。那么接下来我们就重点关注一下这个 render 代码串的生成过程。

    generate

    1. const code = generate(ast, options)

    generate 函数的定义在 src/compiler/codegen/index.js 中:

    1. export function generate (
    2. ast: ASTElement | void,
    3. options: CompilerOptions
    4. ): CodegenResult {
    5. const state = new CodegenState(options)
    6. const code = ast ? genElement(ast, state) : '_c("div")'
    7. return {
    8. render: `with(this){return ${code}}`,
    9. staticRenderFns: state.staticRenderFns
    10. }
    11. }

    generate 函数首先通过 genElement(ast, state) 生成 code,再把 codewith(this){return ${code}}} 包裹起来。这里的 stateCodegenState 的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 genElement

    1. export function genElement (el: ASTElement, state: CodegenState): string {
    2. if (el.staticRoot && !el.staticProcessed) {
    3. return genStatic(el, state)
    4. } else if (el.once && !el.onceProcessed) {
    5. return genOnce(el, state)
    6. } else if (el.for && !el.forProcessed) {
    7. return genFor(el, state)
    8. } else if (el.if && !el.ifProcessed) {
    9. return genIf(el, state)
    10. } else if (el.tag === 'template' && !el.slotTarget) {
    11. return genChildren(el, state) || 'void 0'
    12. } else if (el.tag === 'slot') {
    13. return genSlot(el, state)
    14. } else {
    15. // component or element
    16. let code
    17. if (el.component) {
    18. code = genComponent(el.component, el, state)
    19. } else {
    20. const data = el.plain ? undefined : genData(el, state)
    21. const children = el.inlineTemplate ? null : genChildren(el, state, true)
    22. code = `_c('${el.tag}'${
    23. data ? `,${data}` : '' // data
    24. }${
    25. children ? `,${children}` : '' // children
    26. })`
    27. }
    28. // module transforms
    29. for (let i = 0; i < state.transforms.length; i++) {
    30. code = state.transforms[i](el, code)
    31. }
    32. return code
    33. }
    34. }

    基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,在我们的例子中,我们先了解一下 genForgenIf

    genIf

    1. export function genIf (
    2. el: any,
    3. state: CodegenState,
    4. altGen?: Function,
    5. altEmpty?: string
    6. ): string {
    7. el.ifProcessed = true // avoid recursion
    8. return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
    9. }
    10. function genIfConditions (
    11. conditions: ASTIfConditions,
    12. state: CodegenState,
    13. altGen?: Function,
    14. altEmpty?: string
    15. ): string {
    16. if (!conditions.length) {
    17. return altEmpty || '_e()'
    18. }
    19. const condition = conditions.shift()
    20. if (condition.exp) {
    21. return `(${condition.exp})?${
    22. genTernaryExp(condition.block)
    23. }:${
    24. genIfConditions(conditions, state, altGen, altEmpty)
    25. }`
    26. } else {
    27. return `${genTernaryExp(condition.block)}`
    28. }
    29. // v-if with v-once should generate code like (a)?_m(0):_m(1)
    30. function genTernaryExp (el) {
    31. return altGen
    32. ? altGen(el, state)
    33. : el.once
    34. ? genOnce(el, state)
    35. : genElement(el, state)
    36. }
    37. }

    genIf 主要是通过执行 genIfConditions,它是依次从 conditions 获取第一个 condition,然后通过对 condition.exp 去生成一段三元运算符的代码,: 后是递归调用 genIfConditions,这样如果有多个 conditions,就生成多层三元运算逻辑。这里我们暂时不考虑 v-once 的情况,所以genTernaryExp 最终是调用了 genElement

    在我们的例子中,只有一个 conditionexpisShow,因此生成如下伪代码:

    1. return (isShow) ? genElement(el, state) : _e()

    genFor

    1. export function genFor (
    2. el: any,
    3. state: CodegenState,
    4. altGen?: Function,
    5. altHelper?: string
    6. ): string {
    7. const exp = el.for
    8. const alias = el.alias
    9. const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
    10. const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
    11. if (process.env.NODE_ENV !== 'production' &&
    12. state.maybeComponent(el) &&
    13. el.tag !== 'slot' &&
    14. el.tag !== 'template' &&
    15. !el.key
    16. ) {
    17. state.warn(
    18. `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
    19. `v-for should have explicit keys. ` +
    20. `See https://vuejs.org/guide/list.html#key for more info.`,
    21. true /* tip */
    22. )
    23. }
    24. el.forProcessed = true // avoid recursion
    25. return `${altHelper || '_l'}((${exp}),` +
    26. `function(${alias}${iterator1}${iterator2}){` +
    27. `return ${(altGen || genElement)(el, state)}` +
    28. '})'
    29. }

    genFor 的逻辑很简单,首先 AST 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。

    在我们的例子中,expdataaliasitemiterator1 ,因此生成如下伪代码:

    1. _l((data), function(item, index) {
    2. return genElememt(el, state)
    3. })

    genData & genChildren

    再次回顾我们的例子,它的最外层是 ul,首先执行 genIf,它最终调用了 genElement(el, state) 去生成子节点,注意,这里的 el 仍然指向的是 ul 对应的 AST 节点,但是此时的 el.ifProcessed 为 true,所以命中最后一个 else 逻辑:

    1. // component or element
    2. let code
    3. if (el.component) {
    4. code = genComponent(el.component, el, state)
    5. } else {
    6. const data = el.plain ? undefined : genData(el, state)
    7. const children = el.inlineTemplate ? null : genChildren(el, state, true)
    8. code = `_c('${el.tag}'${
    9. data ? `,${data}` : '' // data
    10. }${
    11. children ? `,${children}` : '' // children
    12. })`
    13. }
    14. // module transforms
    15. for (let i = 0; i < state.transforms.length; i++) {
    16. code = state.transforms[i](el, code)
    17. }
    18. return code

    这里我们只关注 2 个逻辑,genDatagenChildren

    • genData
    1. export function genData (el: ASTElement, state: CodegenState): string {
    2. let data = '{'
    3. // directives first.
    4. // directives may mutate the el's other properties before they are generated.
    5. const dirs = genDirectives(el, state)
    6. if (dirs) data += dirs + ','
    7. // key
    8. if (el.key) {
    9. data += `key:${el.key},`
    10. }
    11. // ref
    12. if (el.ref) {
    13. data += `ref:${el.ref},`
    14. }
    15. if (el.refInFor) {
    16. data += `refInFor:true,`
    17. }
    18. // pre
    19. if (el.pre) {
    20. data += `pre:true,`
    21. }
    22. // record original tag name for components using "is" attribute
    23. if (el.component) {
    24. data += `tag:"${el.tag}",`
    25. }
    26. // module data generation functions
    27. for (let i = 0; i < state.dataGenFns.length; i++) {
    28. data += state.dataGenFns[i](el)
    29. }
    30. // attributes
    31. if (el.attrs) {
    32. data += `attrs:{${genProps(el.attrs)}},`
    33. }
    34. // DOM props
    35. if (el.props) {
    36. data += `domProps:{${genProps(el.props)}},`
    37. }
    38. // event handlers
    39. if (el.events) {
    40. data += `${genHandlers(el.events, false, state.warn)},`
    41. }
    42. if (el.nativeEvents) {
    43. data += `${genHandlers(el.nativeEvents, true, state.warn)},`
    44. }
    45. // slot target
    46. // only for non-scoped slots
    47. if (el.slotTarget && !el.slotScope) {
    48. data += `slot:${el.slotTarget},`
    49. }
    50. // scoped slots
    51. if (el.scopedSlots) {
    52. data += `${genScopedSlots(el.scopedSlots, state)},`
    53. }
    54. // component v-model
    55. if (el.model) {
    56. data += `model:{value:${
    57. el.model.value
    58. },callback:${
    59. el.model.callback
    60. },expression:${
    61. el.model.expression
    62. }},`
    63. }
    64. // inline-template
    65. if (el.inlineTemplate) {
    66. const inlineTemplate = genInlineTemplate(el, state)
    67. if (inlineTemplate) {
    68. data += `${inlineTemplate},`
    69. }
    70. }
    71. data = data.replace(/,$/, '') + '}'
    72. // v-bind data wrap
    73. if (el.wrapData) {
    74. data = el.wrapData(data)
    75. }
    76. // v-on data wrap
    77. if (el.wrapListeners) {
    78. data = el.wrapListeners(data)
    79. }
    80. return data
    81. }

    genData 函数就是根据 AST 元素节点的属性构造出一个 data 对象字符串,这个在后面创建 VNode 的时候的时候会作为参数传入。

    之前我们提到了 CodegenState 的实例 state,这里有一段关于 state 的逻辑:

    1. for (let i = 0; i < state.dataGenFns.length; i++) {
    2. data += state.dataGenFns[i](el)
    3. }

    state.dataGenFns 的初始化在它的构造器中。

    1. export class CodegenState {
    2. constructor (options: CompilerOptions) {
    3. // ...
    4. this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    5. // ...
    6. }
    7. }

    实际上就是获取所有 modules 中的 genData 函数,其中,class modulestyle module 定义了 genData 函数。比如定义在 src/platforms/web/compiler/modules/class.js 中的 genData 方法:

    1. function genData (el: ASTElement): string {
    2. let data = ''
    3. if (el.staticClass) {
    4. data += `staticClass:${el.staticClass},`
    5. }
    6. if (el.classBinding) {
    7. data += `class:${el.classBinding},`
    8. }
    9. return data
    10. }

    在我们的例子中,ul AST 元素节点定义了 el.staticClassel.classBinding,因此最终生成的 data 字符串如下:

    1. {
    2. staticClass: "list",
    3. class: bindCls
    4. }
    • genChildren
      接下来我们再来看一下 genChildren,它的定义在 src/compiler/codegen/index.js 中:
    1. export function genChildren (
    2. el: ASTElement,
    3. state: CodegenState,
    4. checkSkip?: boolean,
    5. altGenElement?: Function,
    6. altGenNode?: Function
    7. ): string | void {
    8. const children = el.children
    9. if (children.length) {
    10. const el: any = children[0]
    11. if (children.length === 1 &&
    12. el.for &&
    13. el.tag !== 'template' &&
    14. el.tag !== 'slot'
    15. ) {
    16. return (altGenElement || genElement)(el, state)
    17. }
    18. const normalizationType = checkSkip
    19. ? getNormalizationType(children, state.maybeComponent)
    20. : 0
    21. const gen = altGenNode || genNode
    22. return `[${children.map(c => gen(c, state)).join(',')}]${
    23. normalizationType ? `,${normalizationType}` : ''
    24. }`
    25. }
    26. }

    在我们的例子中,li AST 元素节点是 ul AST 元素节点的 children 之一,满足 (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot') 条件,因此通过 genElement(el, state) 生成 li AST元素节点的代码,也就回到了我们之前调用 genFor 生成的代码,把它们拼在一起生成的伪代码如下:

    1. return (isShow) ?
    2. _c('ul', {
    3. staticClass: "list",
    4. class: bindCls
    5. },
    6. _l((data), function(item, index) {
    7. return genElememt(el, state)
    8. })
    9. ) : _e()

    在我们的例子中,在执行 genElememt(el, state) 的时候,el 还是 li AST 元素节点,el.forProcessed 已为 true,所以会继续执行 genDatagenChildren 的逻辑。由于 el.events 不为空,在执行 genData 的时候,会执行 如下逻辑:

    1. if (el.events) {
    2. data += `${genHandlers(el.events, false, state.warn)},`
    3. }

    genHandlers 的定义在 src/compiler/codegen/events.js 中:

    1. export function genHandlers (
    2. events: ASTElementHandlers,
    3. isNative: boolean,
    4. warn: Function
    5. ): string {
    6. let res = isNative ? 'nativeOn:{' : 'on:{'
    7. for (const name in events) {
    8. res += `"${name}":${genHandler(name, events[name])},`
    9. }
    10. return res.slice(0, -1) + '}'
    11. }

    genHandler 的逻辑就不介绍了,很大部分都是对修饰符 modifier 的处理,感兴趣同学可以自己看,对于我们的例子,它最终 genData 生成的 data 字符串如下:

    1. {
    2. on: {
    3. "click": function($event) {
    4. clickItem(index)
    5. }
    6. }
    7. }

    genChildren 的时候,会执行到如下逻辑:

    1. export function genChildren (
    2. el: ASTElement,
    3. state: CodegenState,
    4. checkSkip?: boolean,
    5. altGenElement?: Function,
    6. altGenNode?: Function
    7. ): string | void {
    8. // ...
    9. const normalizationType = checkSkip
    10. ? getNormalizationType(children, state.maybeComponent)
    11. : 0
    12. const gen = altGenNode || genNode
    13. return `[${children.map(c => gen(c, state)).join(',')}]${
    14. normalizationType ? `,${normalizationType}` : ''
    15. }`
    16. }
    17. function genNode (node: ASTNode, state: CodegenState): string {
    18. if (node.type === 1) {
    19. return genElement(node, state)
    20. } if (node.type === 3 && node.isComment) {
    21. return genComment(node)
    22. } else {
    23. return genText(node)
    24. }
    25. }

    genChildren 的就是遍历 children,然后执行 genNode 方法,根据不同的 type 执行具体的方法。在我们的例子中,li AST 元素节点的 children 是 type 为 2 的表达式 AST 元素节点,那么会执行到 genText(node) 逻辑。

    1. export function genText (text: ASTText | ASTExpression): string {
    2. return `_v(${text.type === 2
    3. ? text.expression
    4. : transformSpecialNewlines(JSON.stringify(text.text))
    5. })`
    6. }

    因此在我们的例子中,genChildren 生成的代码串如下:

    1. [_v(_s(item) + ":" + _s(index))]

    和之前拼在一起,最终生成的 code 如下:

    1. return (isShow) ?
    2. _c('ul', {
    3. staticClass: "list",
    4. class: bindCls
    5. },
    6. _l((data), function(item, index) {
    7. return _c('li', {
    8. on: {
    9. "click": function($event) {
    10. clickItem(index)
    11. }
    12. }
    13. },
    14. [_v(_s(item) + ":" + _s(index))])
    15. })
    16. ) : _e()

    总结

    这一节通过例子配合解析,我们对从 ast -> code 这一步有了一些了解,编译后生成的代码就是在运行时执行的代码。由于 genCode 的内容有很多,所以我对大家的建议是没必要把所有的细节都一次性看完,我们应该根据具体一个 case,走完一条主线即可。

    在之后的章节我们会对 slot 的实现做解析,我们会重新复习编译的章节,针对具体问题做具体分析,有利于我们排除干扰,对编译过程的学习有更深入的理解。

    原文: https://ustbhuangyi.github.io/vue-analysis/compile/codegen.html