Vue源码阅读(14):代码生成器

 我的开源库:

代码生成器是模板编译的最后一步,所做的工作是根据 AST(抽象语法树)生成代码字符串,这个代码字符串就是 render 函数的代码内容,代码字符串可以通过 new Function(code) 生成真正的 render 函数。

render 函数的作用是生成 vnode(虚拟 DOM),vnode 可以理解成真实 DOM 的抽象表示,通过 vnode 可以在页面上渲染出对应的真实 DOM,使用 vnode 的好处是:Vue 可以根据新旧 vnode 之间的差异计算出哪些真实 DOM 需要更新,然后只更新需要更新的真实 DOM,这样可以最小程度的操作真实 DOM,提高性能。

接下来,以一个简单的模板字符串为例,看看其所生成的 AST 和代码字符串是什么样子的。

模板字符串:

let template = `
  <div class="container">
    <h1 v-if="isShow">我是标题</h1>
    <ul>
      <li v-for="item in names">{{item}}</li>
    </ul>
  </div>
`

抽象语法树:

let ast = {
  attrsList: [],
  attrsMap: {class: "container"},
  children:[
    {
      attrsList: [],
      attrsMap: {v-if: "isShow"},
      if: "isShow",
      children:[
        {type: 3, text: "我是标题", static: true}
      ],
      ifConditions:[
        {exp: "isShow"}
      ],
      ifProcessed: true,
      plain: true,
      static: false,
      staticRoot: false,
      tag: "h1",
      type: 1
    },
    {
      attrsList: [],
      attrsMap: {},
      children: [
        {
          alias: "item",
          attrsList: [],
          attrsMap: {v-for: "item in names"},
          children: [
            {type: 2, expression: "_s(item)", text: "{{item}}", static: false}
          ],
          for: "names",
          forProcessed: true,
          plain: true,
          static: false,
          staticRoot: false,
          tag: "li",
          type: 1
        }
      ],
      plain: true,
      static: false,
      staticRoot: false,
      tag: "ul",
      type: 1
    }
  ],
  parent: undefined,
  plain: false,
  static: false,
  staticClass: ""container"",
  staticRoot: false,
  tag: "div",
  type: 1
}

模板字符串:

let code = `
  with (this) {
    return _c(
      'div',
      {staticClass: 'container'},
      [
        (isShow)?_c('h1', [_v('我是标题')]):_e(),
        _c('ul',_l(
          (names),
          function(item){
            return _c('li',[_v(_s(item))])
          }
        ))
      ]
    )
  }
`

1,render 函数是如何生成虚拟 DOM 的?

render 函数的内部调用 _c、_v、_s 之类的辅助函数生成虚拟 DOM。

1-1,这些辅助函数被定义在哪里?

_c 被定义在 Vue 的实例上。

export function initRender (vm: Component) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
}

从上面代码可知:_c 函数的内部是通过调用 createElement 函数实现生成虚拟 DOM 的功能的,而且 _c 被定义到了 Vue 的实例上面。

其他一些辅助函数被定义在 Vue 的原型对象中,看下面的源码:

export function renderMixin (Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype)
}
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
}

从上面,我们可以得知:除了 _c 的其他辅助函数是定义在 Vue 的原型对象中的。

1-2,render 函数内部是如何调用到这些辅助函数的?

从上一小节可知:创建 vnode 的辅助函数被定义在了 Vue 的实例和原型对象中。这也就说明,可以通过 Vue 实例获取到这些辅助函数。

接下来需要了解的是,with 关键字的作用,借助 with,可以改变 with 后面代码块的作用域,上面 render 函数的内部借助 with,将代码块中的作用域指向 this。接下来,主要看看这个 this 到底指向的是什么。

首先看看 render 函数的调用时机:

export function renderMixin (Vue: Class<Component>) {
  Vue.prototype._render = function (): VNode {
    const { render, _parentVnode } = vm.$options
    vnode = render.call(vm._renderProxy, vm.$createElement)
  }
}

render 函数使用 call 执行代码体,render 函数内部的 this 指向 vm._renderProxy,接下来看看 vm._renderProxy 指向的是什么。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this

    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
  }
}

从上面两处源码可知,render 函数执行时,其内部的 this 就是 vm(Vue 实例),又因为 redner 函数的内部使用 with 关键字将代码块的作用域指向了 this,所以 render 函数的作用域指向的就是 vm。因此,render 函数内是能直接调用的 _c、_v、_s 这些函数的。

2,代码生成器的源码解析

上一小节,我们知道了,render 函数生成 vnode 的原理。接下来看看代码生成器的源码。

注意:这一小节以一个简单的例子,理解代码生成器的主线流程即可,没有必要追求一次就把所有的细节搞懂。具体的细节等到后面分析具体的特性再详细分析。

 以文章一开始的模板字符串为例,看代码生成器的运行机制:

let template = `
  <div class="container">
    <h1 v-if="isShow">我是标题</h1>
    <ul>
      <li v-for="item in names">{{item}}</li>
    </ul>
  </div>
`

2-1,src/compiler/index.js ==> baseCompile():代码生成器的入口

export const createCompiler = createCompilerCreator(
  // 真正执行编译功能的函数,分为三步走:(1)解析器 ==>(2)优化器 ==>(3)代码生成器
  function baseCompile (
    template: string,
    options: CompilerOptions
  ): CompiledResult {
    // 1,解析器。将模板字符串转换成抽象语法树
    const ast = parse(template.trim(), options)
    // 2,优化器。遍历抽象语法树,标记静态节点,
    // 因为静态节点是不会变化的,所以重新渲染视图的时候,能够直接跳过静态节点,提升效率。
    optimize(ast, options)
    // 3,代码生成器。使用抽象语法树生成渲染函数字符串
    const code = generate(ast, options)
    return {
      ast,
      render: code.render,
      staticRenderFns: code.staticRenderFns
    }
  }
)

parse 函数是解析器,optimize 函数是优化器,generate 函数就是本文讨论的代码生成器,参数是优化过后的抽象语法树 AST 和配置对象 options。

generate 函数在 src/compiler/codegen/index.js 文件中,接下来看看该文件中的内容。

2-2,src/compiler/codegen/index.js

代码生成器的大部分源码都在这个文件中,首先看下该文件代码的主体结构。

// 代码生成器。AST ==> render 函数代码字符串
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 对 ast 进行判断。
  // 如果其存在的话,就执行 genElement(ast, state) 生成代码字符串,
  // 否则的话,直接返回 '_c("div")',创建空的 div。
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    // 将生成的代码字符串(code)拼接到 `with(this){return ${code}}`,形成最终的代码字符串
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

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) {
    // 用于处理 v-if
    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 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
  }
}

function genStatic (el: ASTElement, state: CodegenState): string {}

function genOnce (el: ASTElement, state: CodegenState): string {}

export function genIf (el: any, state: CodegenState, altGen?: Function, altEmpty?: string): string {}

function genIfConditions (conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string): string {}

export function genFor (el: any, state: CodegenState, altGen?: Function, altHelper?: string): string {}


export function genData (el: ASTElement, state: CodegenState): string {}

function genDirectives (el: ASTElement, state: CodegenState): string | void {}

function genInlineTemplate (el: ASTElement, state: CodegenState): ?string {}

function genScopedSlots (slots: { [key: string]: ASTElement }, state: CodegenState): string {}

function genScopedSlot (key: string, el: ASTElement, state: CodegenState): string {}

function genForScopedSlot (key: string, el: any, state: CodegenState): string {}

export function genChildren (el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function): string | void {}

function getNormalizationType (children: Array<ASTNode>, maybeComponent: (el: ASTElement) => boolean): number {}

function genNode (node: ASTNode, state: CodegenState): string {}

export function genText (text: ASTText | ASTExpression): string {}

export function genComment (comment: ASTText): string {}

function genSlot (el: ASTElement, state: CodegenState): string {}

function genComponent (componentName: string, el: ASTElement, state: CodegenState): string {}

function genProps (props: Array<{ name: string, value: string }>): string {}

Vue 将生成不同特性代码字符串的工作都细致的拆分到了对应的函数中。这样,当处理到对应特性的 AST 节点的时候,调用对应的处理函数就可以了。

在众多的函数中,有两个函数应该重点关注:

  • 第一个是作为代码生成器入口的 generate 函数,该函数以 AST 抽象语法树为参数,返回生成的代码字符串。
  • 另一个是作为代码生成器主线的 genElement 函数,该函数通过判断当前处理的元素 AST 节点的属性执行对应的函数。

2-3,src/compiler/codegen/index.js ==> generate()

// 代码生成器。AST ==> render 函数代码字符串
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 对 ast 进行判断。
  // 如果其存在的话,就执行 genElement(ast, state) 生成代码字符串,
  // 否则的话,直接返回 '_c("div")',创建空的 div。
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    // 将生成的代码字符串(code)拼接到 `with(this){return ${code}}`,形成最终的代码字符串
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

generate 函数的内部首先判断 ast 是否存在,如果存在的话,调用 genElement 生成对应的代码字符串,如果不存在的话,则直接返回 _c("div")。

注意,这里 genElement 生成的代码字符串还不是完整的 render 函数的代码字符串,genElement 函数生成的 code 还需要拼接到 `with(this){return ${code}}` 中才是完整的 render 函数代码字符串。

接下来看看 genElement 函数的具体内容。

2-4,src/compiler/codegen/index.js ==> genElement()

genElement 函数根据 AST 节点的属性,调用对应的函数生成对应的代码字符串。

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) {
    // 用于处理 v-if
    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 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
  }
}

接下来看看,例子中能够调用到的 genData()、genChildren()、genIf()、genFor() 函数。

2-5,src/compiler/codegen/index.js ==> genData()

genData() 函数的简要代码如下所示:

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
    ......

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // attributes
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:{${genProps(el.props)}},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false, state.warn)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
    ......
  return data
}

genData() 函数的作用是:将 AST 节点中保存的开始标签的相关属性转换成对象字符串的形式。例如我们有如下的模板字符串。

<div class="container" key="containerKey" ref="containerRef"></div>

那么上面这个 div 开始标签中的属性将会转换成如下的对象字符串。

"{key:"containerKey",ref:"containerRef",staticClass:"container"}"

genData() 的工作原理也很简单,就是判断 AST 节点中有没有对应的属性,如果有的话,就拼接对应的 key、value 键值对字符串到变量 data 的后面。

2-6,src/compiler/codegen/index.js ==> genChildren()

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    ......
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${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 方法生成该子元素对应的代码字符串,children 数组通过 map() 和 genNode() 方法生成了对应的代码字符串的数组,然后拼接成一个字符串 return 出去。

return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`

genNode() 方法的内部根据 AST 节点的类型调用对应的函数生成对应的代码字符串,AST 节点有三种类型:

  • type1:元素节点
  • type2:含有表达式的文本节点
  • type3:纯文本节点

根据不同的类型分别调用 genElement()、genComment()、genText()。

genComment() 和 genText() 函数的源码如下所示:

export function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}

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

2-7,src/compiler/codegen/index.js ==> genIf()

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // ifProcessed 属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。
  el.ifProcessed = true // avoid recursion
  return 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() 函数首先将 el 的 ifProcessed 设置为 true,el 的这个属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。将 ifProcessed 设置为 true 后,执行 genIfConditions 进行真正的 v-if 处理。

genIfConditions() 函数的处理逻辑是生成三元表达式的代码字符串,例如:我们上面例子中的 v-if。

<h1 v-if="isShow">我是标题</h1>

这个模板字符串会生成如下的代码字符串。

(isShow)?_c('h1',[_v(_s(title))]):_e()

?前面的内容使用 condition.exp,condition.exp 就是模板字符串中的 'isShow'。

:前面的内容是指 'isShow' 为 true 的话,应该显示的内容,从模板字符串中可知,如果 'isShow' 为 true 的话,应该渲染 <h1>我是标题</h1> 元素节点,所以 :前面的内容应该是能够生成 <h1>我是标题</h1> 元素节点的 vnode 的 render 代码字符串。在 genIfConditions() 方法中使用 genTernaryExp(condition.block) 生成。

function genTernaryExp (el) {
  return altGen
    ? altGen(el, state)
    : el.once
      ? genOnce(el, state)
      : genElement(el, state)
}

在我们的例子中,genTernaryExp() 内部会通过调用 genElement(el, state) 生成 

_c('h1',[_v(_s(title))])

:后面的内容是指 'isShow' 为 false 的话页面显示的内容,在我们的例子中,如果 'isShow' 为 false 的话,不显示任何内容,所以生成的代码字符串中 :后面的内容是 _e(),_e() 函数的意思是创建空的 vnode 节点。

target._e = createEmptyVNode

2-8,src/compiler/codegen/index.js ==> genFor()

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 // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

在我们的例子中,有如下的 v-for 模板字符串:

<ul>
  <li v-for="item in names">{{item}}</li>
</ul>

对应生成的代码字符串如下所示:

_c('ul', _l((names), function (item) {
  return _c('li', [_v(_s(item))])
}))

可以发现生成的代码字符串的层级关系和模板字符串中 ul 和 li 的层级关系不一样,在模板字符串中,ul 标签的子级就是 li,但由于 li 使用了 v-for,所以生成的代码字符串中,_c('ul', ) 的第二个参数并不是 _c('li', ),而是 _l((names), function (item) {}),_c('li', ) 会从匿名函数中 return 出来。这是使用了 v-for 模板字符串生成的代码字符串的一个特点。

接下来说说 genFor() 函数的内部的处理逻辑,genFor() 函数很简单,首先从 AST 节点对象中取出已经被解析好的 v-for 的相关属性(exp、alias、iterator1、iterator2),然后使用 genElement(el, state) 生成 '<li>{{item}}</li>' 对应的代码字符串 _c('li', [_v(_s(item))])。最后,把属性和生成的代码字符串拼接成一个字符串,再 return 出去即可。

return `${altHelper || '_l'}((${exp}),` +
  `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
  '})'

3,总结

代码生成器的流程就是根据 AST 节点对象的属性判断该 AST 节点的类型,然后调用对应的函数生成代码字符串,逐步拼接成完整 render 函数代码字符串的过程。

代码生成器的内容挺多的,但是代码的逻辑却很清晰,因为 Vue 将生成不同特性代码字符串的工作都细致的拆分到了对应的函数中,每个函数都针对处理对应的特性,所以源码理解起来并不算太晦涩。

在这里强烈建议读者先写一个比较简单的模板字符串,例如我们上面作为例子的模板字符串,然后利用浏览器的 debugger 调试代码走一遍,这样的话,就可以对代码生成器的总体流程有了直观的了解,如果想了解某个特性代码生成器是如何处理的话,就向模板字符串中写入这个特性(例如上面例子中的 v-if、v-for),然后再调试走一遍。通过这种方式,我们就可以简单轻快的理解代码生成器的源码。

注意,千万不要追求一次就理解代码生成器的全部内容,每次理解自己关注的特性即可。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值