源码分析:Vue 编译(compile)核心流程之codegen

1,035 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue编译的核心主要包括三步:生成AST树,优化AST树,生成最终代码。之前我们分析了Vue编译核心流程的parse过程(点击这里跳转),optimize过程(点击这里跳转)。本节我们来分析最后一个过程:genCode

codegen(代码生成)

genCode的过程是较为复杂的,内容较多,这里我们以简单的场景的方式来分析下,genCode的整体流程:

例如模板中的定义:

<template>
  <div :class="isBlack" class="red" v-if="isShowTag">
    {{message}}:{{age}}
  </div>
</template>

上面这个模板最终生成的code是:

with(this) {
  return (isShowTag) ? 
    _c('div', {
         staticClass: 'red',
         class: isBlack
        },
        [_v(_s(message) + ":" + _s(age))])
    ) : _e()
}

可以看到这里面有_c_v_e_s等函数,其中_c是定义在src/core/instance/render.js中:

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

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

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

其实这些是一些辅助函数的简写形式,例如_vcreateTextVNode,创建一个文本节点,_stoString,转换成字符串,_ecreateEmptyVNode,创建一个空节点。

下面我们来看下code代码是如何通过template生成的:

generate

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 生成code
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

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
  }
}

code生成的核心函数是genElement

genElement

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    // ast树中有staticRoot属性,执行genStatic函数
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    // ast树中有once属性,进入该逻辑
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    // ast树中有for属性,进入该逻辑
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    // ast树中有if属性,进入该逻辑
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    // 如果是tag是template
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    // slot标签,进入该逻辑
    return genSlot(el, state)
  } else {
    // component or element
    // 组件或者是元素进入该逻辑
    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}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

本案例中,有if标签,首先会执行genIf(el, state)函数

genIf

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // isProcessed属性赋值为true,避免循环执行genIf函数
  el.ifProcessed = true // avoid recursion
  // 执行genIfConditions,ifConditions是个数组,包含ast节点属性对象,之前的文章介绍过
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 如果ASTIfConditions数组为空,直接返回空节点
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  // 取出第一个值
  const condition = conditions.shift()
  // 有表达式的情况下,案例中的exp为isShowTag,
  // 返回值为`(isShowTag)?${genElement(el, state)}:${genIfConditions(conditions, state, altGen, altEmpty)}`
  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的逻辑后会生成一个三元表达式:

`(isShowTag)?${genElement(el, state)}:${genIfConditions(conditions, state, altGen, altEmpty)}`

这里会继续递归执行genElement函数及genIfConditions函数;

案例中再次执行genIfConditions函数后,ASTIfConditions数组为空,直接会返回_e(),所以三元表达式为:

`(isShowTag)?${genElement(el, state)}:_e()`

继续递归执行genElement函数,这时候会进入节点为元素的判断条件逻辑:

// 元素
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
})`

首先会执行genData方法,

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 + ','

  // key
  if (el.key) {
    ...
  }
  // ref
  if (el.ref) {
    ...
  }
  if (el.refInFor) {
    ...
  }
  // pre
  if (el.pre) {
    ...
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    ...
  }
  // module data generation functions
  // 本案例会执行这个函数,`staticClass:red,class:isBlack`
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  
  ......
  data = data.replace(/,$/, '') + '}'
  ......
  // 本案例返回结果 `{staticClass:red,class:isBlack}`
  return data
}

dataGenFns的定义:

this.dataGenFns = pluckModuleFunction(options.modules, 'genData')

function genData (el: ASTElement): string {
  let data = ''
  // 本案例中staticClass是"red"
  if (el.staticClass) {
    data += `staticClass:${el.staticClass},`
  }
  // 本案例中classBinding是表达式isBlack
  if (el.classBinding) {
    data += `class:${el.classBinding},`
  }
  // 返回的结果是 `staticClass:red,class:isBlack`
  return data
}

到此时,第二次genElement的生成结果为:

code = `_c('div', {staticClass:red,class:isBlack}, ${
  children ? `,${children}` : '' // children
})`

完成genData后,会执行genChildren,生成子节点的code,我们继续往下看:

genChildren

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    // 不进入这个逻辑
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    // 这是规范化,直接跳过
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    // altGenNode为空执行genNode
    const gen = altGenNode || genNode
    // 使用map遍历children数组执行gen函数,并将最终结果拼接起来,
    // 这儿的返回结果为 `[_v(_s(message) + ":" + _s(age))])`
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    // 如果子节点是元素节点,继续执行genElement
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    // 如果子节点是注释节点,执行genComment
    return genComment(node)
  } else {
    // 本案例中是文本节点,进入这个方法
    return genText(node)
  }
}

export function genText (text: ASTText | ASTExpression): string {
  // type为2是表达式节点,也就是插值表达式里面定义的表达式,否则就是普通文本节点
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

到此时,第二次genElement的生成结果为:

code = `_c('div', {
            staticClass:red,
            class:isBlack
          }, 
          [_v(_s(message) + ":" + _s(age))])
)`

结合第一次genELement的执行结果:

`(isShowTag)?${genElement(el, state)}:_e()`,

最终生成的代码串:

(isShowTag) ? 
    _c('div', {
         staticClass: 'red',
         class: isBlack
        },
        [_v(_s(message) + ":" + _s(age))])
    ) : _e()

所以最终的render函数:

render: `with(this){return ${code}}`,
// 本案例中
render: `with(this){return (isShowTag) ? 
            _c('div', {
                 staticClass: 'red',
                 class: isBlack
                },
                [_v(_s(message) + ":" + _s(age))])
            ) : _e()}`

这就是genCode的最终执行结果。 可以看到这里是个字符串,函数怎么会是个字符串呢?

其实在编译入口解析这篇文章中我们分析过,最终这条字符串,会通过new Function(code)的方法生成匿名函数。

总结

code生成的过程,其实就是遍历ast树,结合ast树的属性,递归执行genElement函数,拼接生成代码字符串。

到此为止,编译的三大流程,大致的给大家介绍完了,有想了解的,欢迎留言一起探讨。