# 代码生成阶段
用户编写的模板字符串,在经过模板编译阶段生成AST
,在优化阶段标记AST
中的静态节点和静态根节点后,就要进入代码生成阶段了。
代码生成阶段,顾名思义,就是将之前生成的AST
生成render
函数字符串的阶段。Vue 通过调用render
函数字符串,就可以生成vnode
,供后续渲染使用。
# 通过AST生成render函数
生成render
函数的过程其实就是一个递归的过程,从顶向下依次递归AST
中的每一个节点,根据不同的AST
节点类型创建不同的vnode
类型。
我们通过一个示例来说明下,假如现在有一个模板字符串如下,需要生成render
函数
<div id="NLRX"><p>Hello {{name}}</p></div>
成功
该模板经过模板编译阶段和优化阶段后对应的AST
如下:
{ 'type': 1, 'tag': 'div', 'attrsList': [ { 'name':'id', 'value':'NLRX', } ], 'attrsMap': { 'id': 'NLRX', }, 'static':false, 'parent': undefined, 'plain': false, 'children': [{ 'type': 1, 'tag': 'p', 'plain': false, 'static':false, 'children': [ { 'type': 2, 'expression': '"Hello "+_s(name)', 'text': 'Hello {{name}}', 'static':false, } ] }] }
成功
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
接下来我们就来对照已有的模板和AST实际演示一下生成render
函数的过程。
- 首先,根节点
type: 1
说明是一个元素节点,tag
为div
说明是个div
标签。那我们就要创建一个div
的元素节点。我们假设创建一个元素型VNode
的方法叫做_c(tagName,data,children)
,那么可以生成如下代码:
_c('div',{attrs:{"id":"NLRX"}},[/*子节点列表*/])
成功
- 接下来发现根节点有子节点
children
,并且子节点是元素节点p
。同理可得:
_c('div',{attrs:{"id":"NLRX"}},[_c('p'),[/*子节点列表*/]])
成功
- 继续往下,元素节点
p
还有子节点children
,子节点是文本节点,那就创建一个文本型VNode并将其插入到p节点的子节点列表中,这个方法我们先定义为_v()
_c('div',{attrs:{"id":"NLRX"}},[_c('p'),[_v("Hello "+_s(name))]])
成功
- 到此,整个
AST
就遍历完毕了,我们将得到的函数字符串再包装一下,如下:
` with(this){ return _c( 'div', { attrs:{"id":"NLRX"}, } [ _c('p'), [ _v("Hello "+_s(name)) ] ]) } `
成功
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 最后,将上面得到的这个函数字符串传递给
createFunction
函数(关于这个函数在后面会介绍到),createFunction
函数会帮我们把得到的函数字符串转换成真正的函数,赋给组件中的render
选项,从而就是render
函数了。如下:
res.render = createFunction(compiled.render, fnGenErrors) function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
成功
2
3
4
5
6
7
8
9
10
以上就是一个简单的将模板字符串生成为render
函数的过程,接下来我们看源码来分析具体的实现过程。
# 源码分析
源码在src/compiler/codegen/index.js
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) // ast 不存在,生成一个空div vnode const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } }
成功
2
3
4
5
6
7
8
9
10
11
12
从上述代码中可以看出,实际核心代码是genElement(ast, state)
,通过genElement
生成函数字符串,在return
中包裹with(this){return ${code}}
返回。那我们先分析genElement
函数。
export function genElement (el: ASTElement, state: CodegenState): string { // 静态节点并且未处理过 if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) // 节点具有 once 属性且未被处理过 } else if (el.once && !el.onceProcessed) { return genOnce(el, state) // 节点具有 for 属性且未被处理过 } else if (el.for && !el.forProcessed) { return genFor(el, state) // 节点具有 if 属性且未被处理过 } else if (el.if && !el.ifProcessed) { return genIf(el, state) // 节点是 template 标签且没有 slotTarget 属性 } 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 } }
成功
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
genElement
函数会根据当前 AST 元素节点属性的不同从而执行不同的代码生成函数。初看会感觉判断逻辑相当之多,但实际上虽然需要处理的逻辑较多,但最后由AST
生成的只有三种节点类型,元素节点、文本节点和注释节点。我们只需要分析这三种节点即可。
# 元素节点
生成元素节点的代码在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 })`
成功
2
3
4
5
6
7
8
9
首先,el.plain
是在编译的过程中,如果节点没有属性,就会将其设置为true
。这里我们可以用来判断是否需要处理节点的属性数据。这里用来获取属性的函数为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) { 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}",` } // module data generation functions for (let i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el) } // 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)},` } if (el.nativeEvents) { data += `${genHandlers(el.nativeEvents, true, state.warn)},` } // slot target // only for non-scoped slots if (el.slotTarget && !el.slotScope) { data += `slot:${el.slotTarget},` } // scoped slots if (el.scopedSlots) { data += `${genScopedSlots(el.scopedSlots, state)},` } // component v-model if (el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }},` } // inline-template if (el.inlineTemplate) { const inlineTemplate = genInlineTemplate(el, state) if (inlineTemplate) { data += `${inlineTemplate},` } } data = data.replace(/,$/, '') + '}' // v-bind data wrap if (el.wrapData) { data = el.wrapData(data) } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data) } return data }
成功
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
从上述代码中可以看出genData
函数的实现并不复杂,先给data
赋值{
,然后挨个判断节点存在哪些属性,就将数据拼接在data
中,拼接完成后,再补上}
,一个完整的data
就拼完了。
然后判断el.inlineTemplate
是否存在,inlineTemplate
表示是否包含内联的模板内容。如果不存在,则需要使用genChildren
生成子节点列表。简化代码如下
export function genChildren (el): { if (children.length) { return `[${children.map(c => genNode(c, state)).join(',')}]` } } 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) } }
成功
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看出,生成子节点列表children
其实就是遍历AST
的children
属性中的元素,然后根据元素属性的不同生成不同的VNode
创建函数调用字符串。
在data
和 children
处理完成后,将其拼接为函数字符串
`_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
成功
2
3
4
5
从这里,我们就可以知道,_c
有三个参数,分别是节点的标签名tagName
,节点属性data
,节点的子节点列表children
。
# 文本节点
生成文本节点逻辑比较简单,只需要用_v()
函数将文本包裹起来。
export function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })` }
成功
2
3
4
5
6
在作为_v
的参数时,会判断文本的类型,如果是动态文本,则用expression
。如果是静态文本,则使用text
。
为什么在Vue中,经常使用JSON.stringify() 包裹字符串?
为了保持文本格式的统一,动态文本格式为'"hello" + _s(name)'
,静态文本格式为"hello world"
,我们需要将静态文本格式与动态文本格式统一,所以使用JSON.stringify
给文本包装一层字符串,格式为'"hello world"'
# 注释节点
注释节点更为简单,直接使用_e()
包裹注释文本即可
export function genComment (comment: ASTText): string { return `_e(${JSON.stringify(comment.text)})` }
成功
2
3
# _c、_v和_e究竟是什么
有的同学可能已经发现了,上述生成函数字符串时,我们用到的_c
、_v
、_e
三种函数对应三种节点类型。实际上这三个函数是节点创建方法的别名,对应关系如下:
类型 | 创建方法 | 别名 |
---|---|---|
元素节点 | createElement | _c |
文本节点 | createTextVNode | _v |
注释节点 | createEmptyVNode | _e |
# 总结
本小节是模板编译的最后一个阶段——代码生成阶段,我们知道了代码生成阶段就是将之前生成的AST
生成render
函数字符串的阶段。后续Vue通过render
函数就可以生成对应的虚拟DOM。
在源码中,我们发现虽然判断逻辑较多,但核心都是生成元素节点、文本节点及注释节点三个节点。这三个节点处理完毕后会生成相应的代码字符串,拼接在一起后,被包裹在with(this){return ${code}}
后返回一个代码字符串。最终又通过new Function
的方式生成一个可供执行的render
函数。