vue源码分析(十四) 编译之codegen

10/14/2018 vue源码分析面试

# 1. 概述

​ 我们知道编译总共有三个过程, 即parseoptimizecodegen,前面几章我们已经分析了解析和优化两个过程,接下来我们分析编译的最后一个过程就是把优化后的 AST 树转换成可执行的代码,在分析前我们还是以一个 case 进行讲解。

​ 案例如下:

<ul :class="classObject" class="list" v-if="isShow">
  <li v-for="(l, i) in list" :key="i" ref="i" @click="clickItem(index)">{{ i }}:{{ l }}</li>
</ul>
1
2
3

​ 这段模板经过解析和优化两个过程生成的 AST 树如下:

{
  attrsList: [],
  attrsMap: {:class: "classObject", class: "list", v-if: "isShow"},
  children: [
    {
      alias: "l",
      attrsList: [ {name: "@click", value: "clickItem(index)", start: 109, end: 134}],
      attrsMap: {v-for: "(l, i) in list", :key: "i", ref: "i", @click: "clickItem(index)"},
      children: [
        {
          end: 150,
          expression: "_s(i)+":"+_s(l)",
          start: 135,
          static: false,
          text: "{{ i }}:{{ l }}",
          tokens: (3) [{@binding: "i"}, ":", {@binding: "l"}],
          type: 2
        }
      ],
      end: 155,
      events: {click: {value: "clickItem(index)", dynamic: false, start: 109, end: 134}},
      for: "list",
      hasBindings: true,
      iterator1: "i",
      key: "i",
      parent: {/* 省略... ul */},
      plain: false,
      rawAttrsMap: {
        :key: {name: ":key", value: "i", start: 92, end: 100},
				@click: {name: "@click", value: "clickItem(index)", start: 109, end: 134},
				ref: {name: "ref", value: "i", start: 101, end: 108},
				v-for: {name: "v-for", value: "(l, i) in list", start: 69, end: 91},
      },
      ref: ""i"",
      refInFor: true,
      start: 65,
      static: false,
      staticRoot: false,
      tag: "li",
      type: 1
    }
  ],
  classBinding: "classObject",
  end: 171,
  if: "isShow",
  ifConditions: [
     {
       exp: "isShow", 
       block: {
         attrsList: [],
         attrsMap: {:class: "classObject", class: "list", v-if: "isShow"},
         children: [/* 省略... li */],
         classBinding: "classObject",
         end: 171,
         if: "isShow",
         ifConditions: [/* 省略... ul */],
         parent: undefined,
         plain: false,
         rawAttrsMap: {
           :class: {name: ":class", value: "classObject", start: 4, end: 24},
          	class: {name: "class", value: "list", start: 25, end: 37},
          	v-if: {name: "v-if", value: "isShow", start: 38, end: 51}
         },
         start: 0,
         static: false,
         staticClass: ""list"",
         staticRoot: false,
         tag: "ul",
         type: 1
       }
     }
  ],
  parent: undefined,
  plain: false,
  rawAttrsMap: {
    :class: {name: ":class", value: "classObject", start: 4, end: 24},
		class: {name: "class", value: "list", start: 25, end: 37},
		v-if: {name: "v-if", value: "isShow", start: 38, end: 51
  },
  start: 0,
  static: false,
  staticClass: ""list"",
  staticRoot: false,
  tag: "ul",
  type: 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

​ 接下来我们就看 vue 是如何将这样的一个 AST 树生成一个 render 函数代码的。

# 2. generate

​ 我们在 vue源码分析(八) 编译之整体流程 (opens new window) 中分析过了编译的三个过程,即解析模板字符串生成 AST、优化语法树、生成代码,生成代码的过程是通过调用 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
  }
}
1
2
3
4
5
6
7
8
9
10
11

​ 从上面的源码我们可以知道,generate 首先通过 new 创建一个 CodegenState 实例,接着通过 genElementast 对象转换为字符串。

​ 接下来我们分别来分析这两个过程。

# 3. CodegenState

​ 首先我们看一下 CodegenState 的定义,如下:

​ 源码目录:src/compiler/codegen/index.js

export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;
  pre: boolean;

  constructor (options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
    this.pre = false
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

CodegenState 是根据 options 创建的对象,这里的 options 的值为,如下:

{
  comments: true,
  delimiters: undefined,
  outputSourceRange: true,
  shouldDecodeNewlines: false,
  shouldDecodeNewlinesForHref: false,
  warn: (msg, range, tip) {/*...*/}
  __proto__: {
    canBeLeftOpenTag: (val) {/*...*/},
    directives: {
      html: html(el, dir) {/*...*/},
			model: model( el, dir, _warn ) {/*...*/},
			text: text(el, dir) {/*...*/}
    },
    expectHTML: true,
    getTagNamespace: getTagNamespace(tag) {/*...*/},
    isPreTag: (tag) {/*...*/},
    isReservedTag: (tag) {/*...*/},
    isUnaryTag: (val) {/*...*/},
    modules: (3) [
      {
        genData: genData(el) {/*...*/},
        staticKeys: ["staticClass"],
        transformNode: transformNode(el, options) {/*...*/}
      }, {
        genData: genData$1(el) {/*...*/},
        staticKeys: ["staticStyle"],
        transformNode: transformNode$1(el, options) {/*...*/}
      }, {
        preTransformNode: preTransformNode(el, options) {/*...*/}
      }
  	],
    mustUseProp: (tag, type, attr) {/*...*/},
    staticKeys: "staticClass,staticStyle"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

​ 所以根据 options 通过 new 创建的对象实例为,如下:

{
  dataGenFns: [
    genData() {/*...*/}, 
    genData$() {/*...*/}
	],
  directives: {
    bind: bind$1(el, dir) {/*...*/},
    cloak: noop(a, b, c) {/*...*/},
    html: html(el, dir) {/*...*/},
    model: model( el, dir, _warn ) {/*...*/},
    on: on(el, dir) {/*...*/},
    text: text(el, dir) {/*...*/}
  },
  maybeComponent: ƒ (el) {/*...*/},
  onceId: 0,
  options: {/* 省略... */}, // 同上
  pre: false,
  staticRenderFns: [],
  transforms: [],
  warn: warn(msg, range, tip) {/*...*/}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

​ 接下来我们分一下 CodegenState 实例的每个属性的作用:

  • options : 缓存实例化传递进来的 options
  • warn : 用来打印警告信息的
  • transforms : 空数组
  • dataGenFns : 对静态类和静态样式的处理
  • directives : 对指令的相关操作
  • isReservedTag : 保留标签标志
  • maybeComponent : 判断是组件
  • onceId : 使用v-once的递增id
  • staticRenderFns : 对静态根节点的处理
  • pre : v-pre 标识

注意:关于 pluckModuleFunctionoptions.modules 我们在 vue源码分析(十一) 编译之解析(parse)——parse (opens new window) 中已经分析过了。

# 4. 整体流程

​ 我们先以上面的案例来分析,代码生成的整个流程,通过分析 generate我们知道代码生成是通过 genElement 函数将 ast 对象转换为字符串,接下来我们从 genElement 函数来开始分析。

# 4.1 genElement

​ 我们首先看一下 genElement 的定义,如下:

​ 源码目录:src/compiler/codegen/index.js

/**
 * 将ast对象转换为字符串
 * @param {ast树} el 
 * @param {CodegenState 实例} state 
 */
export function genElement (el: ASTElement, state: CodegenState): string {
  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) {
    // 处理 v-once 节点
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    // 处理 v-for 节点
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    // 处理 v-if 节点
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    // 处理 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 {
      //核心的body部分
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 1、生成节点的数据对象data的字符串
        data = genData(el, state)
      }

      // 2、查找其子节点,生成子节点的字符串
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 3、将tag,data,children拼装成字符串
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    // 循环执行 state.transforms 数组中的 genData 函数
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

​ 我们案例中跟节点为 ul ,属性有:class: "classObject"class: "list"v-if: "isShow" 所以代码会执行到 genIf 如下语句:

return genIf(el, state)
1

# 4.2 genIf

​ 接下来我们看看 genIf 的定义,如下:

​ 源码目录:src/compiler/codegen/index.js

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
1
2
3
4
5
6
7
8
9

genIf 的逻辑很简单,主要是通过执行 genIfConditions ,处理 v-if/v-else-if/v-else 指令所对应的 AST 节点。

# 4.3 genIfConditions

​ 接下来我们看看 genIfConditions 的逻辑,首先是判断 AST 树的 conditions 数组是否有值,如下:

​ 源码目录:src/compiler/codegen/index.js

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  /* 省略... */
}
1
2
3
4
5
6
7
8
9
10
11
12

​ 我们通过源码可以知道如果 conditions 数组为空,则返回一个生成空的虚拟 dom 的函数字符串;如果conditions 数组不为空,代码继续往下执行,如下:

​ 源码目录:src/compiler/codegen/index.js

const condition = conditions.shift()
if (condition.exp) {
  return `(${condition.exp})?${
  genTernaryExp(condition.block)
}:${
  genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
  return `${genTernaryExp(condition.block)}`
}
1
2
3
4
5
6
7
8
9
10

​ 当 conditions 数组不为空时,首先取第一个元素赋值给变量 condition,在我们当前案例中,我们知道 conditions 为:

[
  {
    exp: "isShow", 
    block: {/* 省略,ul节点生成的AST树 */}
  }
]
1
2
3
4
5
6

​ 所以接下来的 if 条件 condition.exptrue 继续执行 if 语句块,而此处的 else 语句块是 conditionsv-else 对应的 AST 树执行的逻辑,在我们当前案例中不会执行else 语句块。

if 语句块的作用是调用 genIfConditions 进行递归生成二元表达式。我们从源码可以得到二元表达式的条件为 condition.exp 第一个表达式为 genTernaryExp(condition.block) 第二个表达式为 genIfConditions(conditions, state, altGen, altEmpty),我们接下来看看 genTernaryExp 的定义如下:

​ 源码目录:src/compiler/codegen/index.js

// 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)
}
1
2
3
4
5
6
7
8

genTernaryExp 函数里面主要是一个多元的表达式,首先判断 altGen 如果存在,则返回调用altGen(el, state) 执行的结果,如果不存在接着判断当前 AST 树是否使用了 v-once 指令即判断 el.once 属性是否存在,如果存在则返回调用genOnce(el, state) 执行的结果,即返回一个创建静态标签节点的函数的字符,如果不存在则继续回调 genElement(el, state) 生成节点的字符串。

​ 在我们当前案例中,会执行 genElement(el, state)genElement 我们前面已经分析过了,这次执行 genElement 时,由于我们在 genIfAST 树添加了 el.ifProcessed = true ,所以这次执行 genElement 将不会在执行到 genIf 对应的 if 语句块,而是 执行 else 语句,我们再来回顾一下 genElementelse 语句块,如下:

​ 源码目录:src/compiler/codegen/index.js

// component or element
let code
if (el.component) {
  code = genComponent(el.component, el, state)
} else {
  let data
  if (!el.plain || (el.pre && state.maybeComponent(el))) {
    data = 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

​ 从源码我们可以知道 else 语句块主要是对组件和元素进行处理生成 render 表达式字符串。

​ 这段代码首先定义一个变量 code 用来保存最终生成 render 表达式字符串,接下来是一个 if...else 语句块,其中 if 语句块只要是对组件的处理,else 语句块是对普通元素的处理,在我们这个案例中会执行到else 语句块,首先会判断当前标签是不否存在 plain 属性或者存在 pre属性并且是组件,则会执行 if 语句,此时我们 AST 树没有 plain 属性,所以会调用 genData 函数生成节点的数据对象 data 的字符串。

​ 接下来通过判断标签是不是内联模板则返回 null ,如果不是内联模板则直接调用 genChildren,查找其子节点,生成子节点的字符串。

​ 接着将 tagdatachildren 拼装成字符串,赋值给 code

​ 最后通过循环 state.transforms 数组,执行数组中的每个函数,我们在前面分析此案例生成的 AST 树的时候知道 state.transforms 是通过 this.transforms = pluckModuleFunction(options.modules, 'transformCode') 这句生成的,此时的 state.transforms 为空,所以不会执行循环里面的代码,最后直接返回 code

说明:关于 el.plain 我们在 vue源码分析(十二) 编译之解析(parse)——处理标签 (opens new window) 中已经做了详细的分析。

​ 关于内联模板 inline-template ,可以查看 这里 (opens new window)

​ 关于 pluckModuleFunction 可以查看 这里 (opens new window)

说明:关于 genDatagenChildren 我们会在下面小节中做详细的分析。

# 4.4 genData

​ 接下我们看看 genData 的代码逻辑,如下:

​ 源码目录:src/compiler/codegen/index.js

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 + ','
  
  /* 省略... */
}
1
2
3
4
5
6
7
8
9
10

genData 首先是定义一个 data 变量,用来存储生成节点的数据对象 data 的字符串,接下来调用 genDirectivesdirectives 进行处理将指令对象转换成一个字符串格式,例如 <div v-info></div> 则变成 directives:[{name:"info",rawName:"v-info"}]

​ 接下来判断获取到的指令字符串是否存在,如果存在则将获取的字符串追加到 data 变量并且已逗号结尾,如果不存在直接执行后续的代码。

说明:关于 genDirectives 我们会在下面小节中做详细的分析。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// key
if (el.key) {
  data += `key:${el.key},`
}
// ref
if (el.ref) {
  data += `ref:${el.ref},`
}
if (el.refInFor) {
  data += `refInFor:true,`
}
// pre
if (el.pre) {
  data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
  data += `tag:"${el.tag}",`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

​ 这段代码就是对一些属性的操作,keyrefrefInForpretag 纯粹将他们拼接起来,在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

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

​ 我们在前面的章节中分析知道 pluckModuleFunction 实际上就是获取所有 modules 中的 genDatas 函数,其中, class modulestyle module定义了 genDatas 函数。

​ 下面我们看看这两个 genDatas 函数。

​ 源码目录:src/platforms/web/compiler/modules/class.js

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

​ 源码目录:src/platforms/web/compiler/modules/style.js

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

​ 这个函数的逻辑一样,就是对静态属性 classstyle 和动态属性 :class:style 的处理,把处理结果值拼接起来返回。

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

'staticClass:"list",class:classObject,'
1

​ 我们再回到 genData 函数,继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// attributes
if (el.attrs) {
  data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
  data += `domProps:${genProps(el.props)},`
}
1
2
3
4
5
6
7
8

​ 这段代码是对普通属性和 props 属性进行处理,将结果追加到 data 变量并且已逗号结尾。同样在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,接下来的代码如下:

​ 源码目录:src/compiler/codegen/index.js

// event handlers
if (el.events) {
  data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
  data += `${genHandlers(el.nativeEvents, true)},`
}
1
2
3
4
5
6
7

​ 这段代码是对原生事件和自定义事件进行处理,将结果追加到 data 变量并且已逗号结尾。同样在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
  data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
  data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
1
2
3
4
5
6
7
8
9

​ 这段代码是对非作用域插槽和作用于插槽进行处理,将结果追加到 data 变量并且已逗号结尾。同样在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// component v-model
if (el.model) {
  data += `model:{value:${
  el.model.value
},callback:${
  el.model.callback
},expression:${
  el.model.expression
}},`
}
1
2
3
4
5
6
7
8
9
10

​ 这段代码是在组件上使用 v-model 指令的情况,对 AST 树的 model 属性进行处理,将结果追加到 data 变量并且已逗号结尾。

​ 例如:v-model = "name",则转换的 AST 树为:

{
  callback: "function ($$v) {name=$$v}", 
  expression: ""name"", 
  value: "(name)"
}
1
2
3
4
5

​ 所有最终生成的字符串为,如下:

'model:{value:(name),callback:function ($$v) {name=$$v},expression:"name"},'
1

​ 同样在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// inline-template
if (el.inlineTemplate) {
  const inlineTemplate = genInlineTemplate(el, state)
  if (inlineTemplate) {
    data += `${inlineTemplate},`
  }
}
1
2
3
4
5
6
7

​ 这段代码是对内联模板的处理,将结果追加到 data 变量并且已逗号结尾。同样在我们的例子中, ul AST 元素节点这个属性值是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
  data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
1
2
3
4
5
6
7

​ 这段代码首先通过 replace 替换,将最后一个逗号替换成空字符,并且在 data 字符串末尾添加一个 } 括号,接着判断AST 树是否存在动态属性,例如:

<!-- 动态参数的缩写 (2.6.0+) -->
<div :[key]="name"> ... </div>
1
2

​ 转换的 AST 树为:

dynamicAttrs: [{
  dynamic: true,
  end: 17,
  name: "key",
  start: 5,
  value: "name"
}]
1
2
3
4
5
6
7

​ 如果存在动态参数的属性,最终通过 genProps 处理,,将结果和 data 变量合并,如下:

'_b({staticClass:"list",class:classObject},"ul",_d({},[val,name]))'
1

​ 同样在我们的例子中, ul AST 元素节点这个属性值是 undefined ,所以不会执行 if 中的语句 。

​ 我们继续往下看,代码如下:

​ 源码目录:src/compiler/codegen/index.js

// v-bind data wrap
if (el.wrapData) {
  data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
  data = el.wrapListeners(data)
}
return data
1
2
3
4
5
6
7
8
9

genData 最后这两个 if 语句是对包装数据和包装事件进行处理,例如:

<!-- 绑定一个全是 attribute 的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
1
2
3
4
5

​ 同样在我们的例子中, ul AST 元素节点这些属性值都是 undefined ,所以不会执行 if 中的语句 。

​ 至此我们把 genData 的业务逻辑已经分析完了,最后我们总结一下,genData的主要作用:

  • 通过 genDirectives 对指令进行处理
  • 对一些属性的处理,包括 keyrefrefInForpretag,纯粹将他们拼接起来
  • 对静态属性 classstyle 和动态属性 :class:style 的处理
  • 通过 genPropsattrsprops 属性进行处理
  • 通过 genHandlers 对自定义事件和原生事件进行处理
  • slot 进行处理和通过 genScopedSlots 对作用于插槽进行处理
  • 对些属性 v-model 的处理
  • 通过 genInlineTemplate 内联模板 inline-template 进行处理
  • 对动态属性处理
  • v-bind 绑定的包装数据处理
  • v-on 绑定的包装事件处理

# 4.5 genChildren

​ 我们再回到 genElement 中的 else 语句块,如下:

​ 源码目录:src/compiler/codegen/index.js

// component or element
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
  data = genData(el, state)
}

const children = el.inlineTemplate ? null : genChildren(el, state, true)

code = `_c('${el.tag}'${
	data ? `,${data}` : '' // data
}${
	children ? `,${children}` : '' // children
})`
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 前面分析过了 genData ,接下通过判断内联模板是否存在,如果存在则 children 为空, 如果不存在则 childrengenChildren(el, state, true) ,在我们当前案例中内联模板不存在所以执行 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.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'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

genChildren 首先获取子节点 AST 树赋值给 children,接下来判断 children 数组是否有值,如果有值,则继续执行下面逻辑:

  • v-for 进行简单优化
  • 对其他子节点进行处理

​ 在们当前的案例中,此时的 childrenli AST 树,因为 li AST 元素节点是 ul AST 元素节点的 children 之一,满足 children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot' 条件,if 语句中先判断 checkSkip,如果为 falsenormalizationType = '', 如果为 true,再判断是不是组件,如果是则 normalizationType = ',1', 否则 normalizationType = ',0',最后返回 genElement 生成 li AST 元素节点的代码并拼接 normalizationType 的值。

​ 再回到了我们之前分析的 genElement 函数。此时会执行到下面的逻辑,如下:

​ // 如果为 true,在判断是不是组件,如果是则 normalizationType = ',1', 否则 normalizationType = ',0' 因此通过 genElement 生成 li AST 元素节点的代码,也就回到了我们之前分析的 genElement 函数。此时会执行到下面的逻辑,如下:

​ 源码目录:src/compiler/codegen/index.js

return genFor(el, state)
1

​ 这里我们对 genFor 不多做分析,后面单独分析,我们只要知道 genFor 是对 AST 元素节点中和 for 相关的一些属性的处理,然后再次调用 genElement 生成 li AST 元素节点的代码并拼接一些 for 相关的属性,最后返回这个拼接后的代码字符串。

​ 再次执行 genElement 函数的时候,由于在 genFor 中标记了已处理的标记,即 el.forProcessed = true ,所以这次执行 genElement 将不会在执行到 genFor 对应的 if 语句块,而是执行 else 语句。else 语句我们前面已经分析过了,此时又会执行 else 语句中的 genDatagenChildren 函数,参数是 li AST 元素节点。

  • genData

genData 我们前面已经分析过了,这里需要强调一下,在处理 li AST 元素节点时,会执行下面的几个逻辑语句,如下:

// key
if (el.key) {
  data += "key:" + (el.key) + ",";
}
// ref
if (el.ref) {
  // 保存ref到data属性上
  data += "ref:" + (el.ref) + ",";
}
if (el.refInFor) {
  // 保存refInFor到data属性上
  data += "refInFor:true,";
}
// module data generation functions
for (var i = 0; i < state.dataGenFns.length; i++) {
  data += state.dataGenFns[i](el);
}
// event handlers
if (el.events) {
  data += (genHandlers(el.events, false)) + ",";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

​ 这段代码中除了 genHandlers 逻辑我们没有分析,其他逻辑前面都分析过了这里就不重复讲解了。

​ 最后通过 genData 生成的li AST 元素节点的代码字符串,如下:

data = '{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}}'
1
  • genChildren

​ 我们继续看看 genChildrenli AST 元素节点的处理,这次执行 genChildren 获取到的子节点 AST 树为,如下:

[
  {
    "type": 2,
    "expression": "_s(i)+\":\"+_s(l)",
    "tokens": [
      {
        "@binding": "i"
      },
      ":",
      {
        "@binding": "l"
      }
    ],
    "text": "{{ i }}:{{ l }}",
    "start": 135,
    "end": 150,
    "static": false
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

​ 子节点 AST 树为文本节点,所以不会执行 if 语句而是执行一下代码,如下:

​ 源码目录:src/compiler/codegen/index.js

const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode

return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
1
2
3
4
5
6
7
8

​ 首先通过 checkSkip 值来判断 normalizationType(规范化类型) 值的来源,在我们当前案例中,checkSkiptrue ,所以会执行 getNormalizationType 函数,定义如下:

​ 源码目录:src/compiler/codegen/index.js

function getNormalizationType (
  children: Array<ASTNode>,
  maybeComponent: (el: ASTElement) => boolean
): number {
  let res = 0
  for (let i = 0; i < children.length; i++) {
    const el: ASTNode = children[i]
    if (el.type !== 1) {
      continue
    }
    if (needsNormalization(el) ||
        (el.ifConditions && el.ifConditions.some(c => needsNormalization(c.block)))) {
      res = 2
      break
    }
    if (maybeComponent(el) ||
        (el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))) {
      res = 1
    }
  }
  return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

​ 我们通过注视知道 getNormalizationType 的作用是确定子数组所需的规范化,规范化类型有一下三种:

  • 0 : 不需要规范化
  • 1 : 需要简单的规范化(可能是1级深嵌套数组)
  • 2 : 需要完全规范化

​ 我们再来看看 getNormalizationType 的执行逻辑,首先循环子节点,获取的每个子节点。

​ 然后第一个if 语句判断节点类型,如果节点类型为 1,则跳出本次循环,如果节点类型不为 1,则继续往下执行。

​ 接下来第二个if 语句块是对需要简单的规范化(可能是1级深嵌套数组)的节点的逻辑处理,将 res 赋值为 2,需要满足以下条件才会执行,if 语句块:

  • 通过 needsNormalization 来判断节点上有 v-for 或标签名是 templateslot
  • 或者节点是 if 块,并且块内元素有自定义组件的

​ 第三个if 语句块是对需要完全规范化的节点的逻辑处理,将 res 赋值为 1,需要满足以下条件才会执行,if 语句块:

  • el 是自定义组件
  • 或者节点是 if 块,并且块内元素有自定义组件的

​ 我们再来看看 needsNormalization 的定义,如下:

​ 源码目录:src/compiler/codegen/index.js

function needsNormalization (el: ASTElement): boolean {
  return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'
}
1
2
3

needsNormalization 很简单就是判断节点是否需要规范化,即满足一下条件:

  • 节点存在 for 属性
  • 或者节点标签是 template
  • 或者节点标签是 slot

​ 在我们当前案例中,子节点类型为 2,所以在通过 getNormalizationType 获取规范化类型时,执行的是第一个if 语句,得到的 normalizationType 为 0。

​ 我们回到 genChildren继续往下看,接下来是遍历子节点,调用 genNode 函数处理子节点。

genNode 方法,根据不同的 type 执行具体的方法。在我们的例子中,li AST 元素节点的 childrentype2 的表达式 AST 元素节点,那么会执行到 genText(node) 逻辑,最后 genText 生成的字符串为:

"_v(_s(i)+":"+_s(l))"
1
  • li AST 元素节点处理

​ 我们再回到 genChildren 最后将遍历li AST 元素节点的子节点生成的字符串拼接起来,如下:

"[_v(_s(i)+":"+_s(l))]"
1

​ 我们再回到 genElement 中,在 else 语句li AST 元素节点生成的字符串 data,如下:

"{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}}"
1

​ 然后拼接li AST 元素节点生成的字符串 datali AST 元素节点的子节点生成的字符串 children 赋值给变量 code,并返回 code,如下:

"_c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])"
1

​ 我们再回到 genFor 中,将节点的 for 属性的和 code 拼接起来,如下:

"_l((list),function(l,i){return _c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])})"
1

​ 至此li AST 元素节点处理生成的字符串已全部完成,接下来我们继续回到ul AST 元素节点的处理。

  • ul AST 元素节点

​ 我们再回到 genChildren ,这时处理的是ul AST 元素节点的子节点生成的字符串拼接起来,如下:

"_l((list),function(l,i){return _c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])}),0"
1

​ 我们再回到 genElement ,这时对ul AST 元素节点的处理生成的字符串 data ,如下:

"{staticClass:"list",class:classObject}"
1

​ 生成的 children 字符串就是执行 genChildren 函数返回的结果,如上。

​ 然后拼接ul AST 元素节点生成的字符串 dataul AST 元素节点的子节点生成的字符串 children 赋值给变量 code,并返回 code,如下:

"_c('ul',{staticClass:"list",class:classObject},_l((list),function(l,i){return _c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])}),0)"
1

​ 我们再回到 genIfConditions ,此函数中通过递归执行 genIfConditions 来处理 v-if 生成的 AST 树中对应的 ifConditions 数组中的所有节点,最终生成二元表达式,如下:

"(isShow)?_c('ul',{staticClass:"list",class:classObject},_l((list),function(l,i){return _c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])}),0):_e()"
1

​ 我们再回到 generate 函数,将 AST 树生成的代码字符串,拼接成一个 with 语句如下:

"with(this){return (isShow)?_c('ul',{staticClass:"list",class:classObject},_l((list),function(l,i){return _c('li',{key:i,ref:"i",refInFor:true,on:{"click":function($event){return clickItem(index)}}},[_v(_s(i)+":"+_s(l))])}),0):_e()}"
1

​ 至此模板生成的 AST 元素节点处理生成的字符串已全部分析完成。

说明:关于 genForgenHandlersgenNodegenText,我们会在下一小节单独分析。

# 4.6 genFor

# 4.7 genHandlers

# 4.8 genNode

# 4.9 genText

# 4.10 总结

generate调用栈:

调用栈

说明:关于 genDirectivesgenPropsgenScopedSlotsgenInlineTemplate 在我们案例中没有执行到,我们会在后面单独分析。

# 5. 其他生成器

​ 源码目录:src/compiler/codegen/index.js


1

# 5.1 genStatic

# 5.2 genOnce

# 5.3 genDirectives

说明:关于 directives 可以移步到 这里 (opens new window) 学习。

# 5.4 genProps

说明:关于 propsattrs 的区别可以移步到 这里 (opens new window) 学习。

# 5.5 genSlot

# 5.6 genComponent

# 5.7 genScopedSlots

# 5.8 genInlineTemplate

# 6. 参考

Vue源码学习(零):内部原理解析 (opens new window)

VUE源码学习第九篇--编译(generate) (opens new window)