vue3-template-compile
# 一、总览
基于vue3.3.8的最新源码进行分析。
整个分析我们用从上到下的思想来进行。
首先,在 vue 的官网上找到了渲染机制 (opens new window)的说明,如下:
编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用 (opens new window)执行,因此它会追踪其中所用到的所有响应式依赖。
更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
本期我们研究的主要方向就是分析 Vue 模版编译成渲染函数的源码。
按照从上到下的思想,首先找到 packages/vue 的入口 src/index.ts。
将代码简化,留下关键代码如下:
import { compile } from '@vue/compiler-dom'
import { registerRuntimeCompiler } from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom'
function compileToFunction(template, options = {}) {
// compile将传入template, options得到了Vue构建函数
const { code } = compile(template, options)
// 将源码封装到函数内,封装的同时也依赖 runtimeDom
const render = new Function('Vue', code)(runtimeDom)
return render
}
registerRuntimeCompiler(compileToFunction)
export { compileToFunction as compile }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 二、compile总览
接下来,我们对compile函数进行分析,途经compiler-dom的参数补充,最终找到compiler-core下的src/compile.ts
。
将代码简化(默认会把ssr代码、抛出错误、兼容代码等省略),留下关键代码如下:
import { baseParse } from './parse'
import { transform } from './transform'
import { generate } from './codegen'
import { extend } from '@vue/shared'
import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformElement } from './transforms/transformElement'
import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { trackSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
import { transformMemo } from './transforms/vMemo'
export function baseCompile(template, options = {}) {
// 将template源码转换为ast树
const ast = baseParse(template, options)
// 对节点的转换,在数组中的顺序会影响转换的优先级
const nodeTransforms = [
transformOnce,
transformIf,
transformMemo,
transformFor,
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
]
// 对指令的转换,在后续的 buildProps 时会用到
const directiveTransforms = {
on: transformOn,
bind: transformBind,
model: transformModel
}
// 在ast模式下做节点与指令等转换
transform(ast, extend({}, options, {
nodeTransforms,
directiveTransforms
})
)
// 将转换后的代码重新生成为代码
return generate(ast, extend({}, options))
}
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
固然,vue 对模版的编译是没有用到babel的(该处用到babel的地方是利用对表达式的解析来判断node端的表达式),但是编译代码这块的原理是基本类似的。
接下来,我们按照babel的使用依次查看baseParse
、transform
、generate
。
# 三、parse
baseParse 的主要作用是把 Vue 模版转换成 AST 语法树。
在这之前,我们先了解下 AST 的结构进行温习(或学习)。
# 3.1 AST 语法树
这里定义一个 dom 来讲解,如:
<template><div>纯文本</div></template>
使用 https://astexplorer.net/ 作转换,其对应的 AST 为:
// 根节点
const ast = {
"type": 0, // 节点类型,用于标识语法单元或操作。如: ROOT、ELEMENT、TEXT、COMMENT、SIMPLE_EXPRESSION、 INTERPOLATION等
"children": [
{
"type": 1,
"ns": 0, // 指代Namespace属性,用于表示HTML文档中元素节点所属的命名空间,大多数编程语言为空,vue中为0
"tag": "template", // 标签名。如: template、script、div、span
"tagType": 0, // 标签类型。如: ELEMENT, COMPONENT, SLOT, TEMPLATE
"props": [], // 属性集
"isSelfClosing": false, // 是否是自闭合标签
"children": [ // 子节点
{
"type": 1,
"ns": 0,
"tag": "div",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "纯文本", // 要渲染的文本内容
"loc": { // 位置信息
"start": { // 起始位置
"column": 16, // 列位置(换行后会从0开始)
"line": 1, // 行位置
"offset": 15 // 字符位置(与换行无关)
},
"end": { // 结束位置
"column": 19,
"line": 1,
"offset": 18
},
"source": "纯文本" // 源码
}
}
],
"loc": {
"start": { "column": 11, "line": 1, "offset": 10 },
"end": { "column": 25, "line": 1, "offset": 24 },
"source": "<div>纯文本</div>"
}
}
],
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 36, "line": 1, "offset": 35 },
"source": "<template><div>纯文本</div></template>"
}
}
],
"helpers": {}, // 帮助函数,用于存储在转换或编译过程中生成的辅助函数
"components": [], // 组件,用于存储当前模块所依赖或使用到的组件信息,包括组件名称、路径、导入声明等
"directives": [], // 指令,用于存储与当前模块相关联的所有自定义指令信息,包括指令名称、参数、修饰符等
"hoists": [], // 提升项,用于存储需要被提前计算并缓存起来以优化性能的表达式或计算结果
"imports": [], // 导入项,用于描述当前模块所引入的外部模块,并记录其对应关系和可访问性等相关信息
"cached": 0, // 用于缓存一次求值结果,并在后续多次使用时直接返回缓存值
"temps": 0, // 临时变量,用于存储在生成的代码中临时使用的变量,通常是用于辅助某个功能的实现或过程的处理
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 36, "line": 1, "offset": 35 },
"source": "<template><div>纯文本</div></template>"
}
}
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
# 3.2 主流程
现在,我们对 AST 已经有了初步认识,我们继续对 baseParse 进行分析。其主要代码如下:
export function baseParse(content, options = {}) {
// 创建解析上下文
const context = createParserContext(content, options)
// 获取解析起始位置
const start = getCursor(context)
// 创建 AST 根节点
return createRoot(
// 解析 context 下的子节点
parseChildren(context, TextModes.DATA, []),
// 获取解析范围,将返回起始位置、结束位置和源码
getSelection(context, start)
)
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.3 parseChildren
parseChildren 主要是对源代码解析出子节点。
# 3.3.1 parseChildren主流程
我们拎出来,留下核心代码,看看它都做了什么:
function parseChildren(context, mode, ancestors) {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
const nodes = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node = undefined
// 尝试解析出 node
if (startsWith(s, context.options.delimiters[0])) {
// 解析界限符'{{'
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
// 开始标签
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
// 注释标签
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// 解析 DOCTYPE 伪注释
node = parseBogusComment(context)
} else if (startsWith(s, '<![CDATA[')) {
// 解析 CDATA(xml语法,CDATA标签内的纯文本免转义)
if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors)
}
}
} else if (/[a-z]/i.test(s[1])) {
// 解析节点元素
node = parseElement(context, ancestors)
}
}
// 若未解析出 node,则作为纯文本解析
if (!node) {
node = parseText(context, mode)
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
}
/* 省略此处继承vue2对空白的处理 */
return nodes
}
function isEnd(context, mode, ancestors) {
const s = context.source
switch (mode) {
case TextModes.DATA: // 该模式下,包含其他的元素、同时也会存在文本需要转义
if (startsWith(s, '</')) {
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true
}
}
}
break
case TextModes.RCDATA: // 该模式下,标签内的文本需要转义,如:textarea、title
case TextModes.RAWTEXT: { // 该模式下,标签内的文本不需要转义,如:style,iframe,script,noscript
const parent = last(ancestors)
if (parent && startsWithEndTagOpen(s, parent.tag)) {
return true
}
break
}
case TextModes.CDATA: // 该模式对应 XML 的 CDATA
if (startsWith(s, ']]>')) {
return true
}
break
}
return !s
}
// 以结束标签的开始符号开头
function startsWithEndTagOpen(source, tag) {
return (
startsWith(source, '</') &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
/[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
)
}
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
87
88
89
90
节点解析流程图
在整个解析流程中,最重要的无疑是对插值节点、元素节点、文本的解析,我们接下来对它们进行简化与分析。
# 3.3.2 parseInterpolation
该方法主要是对界限符写法的内容进行解析。如:
function parseInterpolation(
context: ParserContext,
mode: TextModes
): InterpolationNode | undefined {
// 对应'{{'和'}}'
const [open, close] = context.options.delimiters
// 查找结束界限符的位置
const closeIndex = context.source.indexOf(close, open.length)
// 获取整个界限符节点对应的当前行对应的位置、行数、字符位置
const start = getCursor(context)
// 根据开始界限符进位
advanceBy(context, open.length)
// 获取界限符节点内部内容对应的当前行对应的位置、行数、字符位置
const innerStart = getCursor(context)
// 获取界限符节点内部内容结束时对应的当前行对应的位置、行数、字符位置
const innerEnd = getCursor(context)
// 界限符节点内部内容的代码长度
const rawContentLength = closeIndex - open.length
// 界限符节点内部内容的代码长度
const rawContent = context.source.slice(0, rawContentLength)
// 解析界限符内部的原始内容
const preTrimContent = parseTextData(context, rawContentLength, mode)
// 原始内容去除前后空格
const content = preTrimContent.trim()
// 有效内容相对于原始内容的开始位置
const startOffset = preTrimContent.indexOf(content)
if (startOffset > 0) {
// 对空白内容进位
advancePositionWithMutation(innerStart, rawContent, startOffset)
}
// 结束界限符的位置
const endOffset =
rawContentLength - (preTrimContent.length - content.length - startOffset)
advancePositionWithMutation(innerEnd, rawContent, endOffset)
// 根据结束界限符进位
advanceBy(context, close.length)
return {
type: NodeTypes.INTERPOLATION, // 插值类型
content: {
type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT, // 非常量类型
content,
loc: getSelection(context, innerStart, innerEnd) // 内容位置信息
},
loc: getSelection(context, start) // 节点位置信息
}
}
function parseTextData(context, length, mode) {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
return rawText
}
// 由于解析产生变化使位置进位
export function advancePositionWithMutation(pos, source, numberOfCharacters = source.length) {
let linesCount = 0 // source代码存在的行数
let lastNewLinePos = -1 // 最后一个换行符的位置
for (let i = 0; i < numberOfCharacters; i++) {
if (source.charCodeAt(i) === 10 /* 换行符的ASCII码 */) {
linesCount++
lastNewLinePos = i
}
}
// 更新位置
pos.offset += numberOfCharacters
// 更新当前行的位置
pos.line += linesCount
// 更新当前列的位置
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters // 只有一行,则直接加上字符串的长度
: numberOfCharacters - lastNewLinePos // 存在多行,则用字符串的长度减去最后一行相对字符串长度的位置
return pos
}
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
# 3.3.3 parseElement
该方法主要是解析节点。如: <div></div>
// 解析节点元素
function parseElement(context, ancestors) {
const parent = last(ancestors)
// 开始标签
const element = parseTag(context, TagType.Start, parent)
// 对于子节点而言,当前元素是最后的祖先元素
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
// 把当前节点作为祖先节点,继续解析子节点,parseChildren实现了递归
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
element.children = children
// 结束标签
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
}
element.loc = getSelection(context, element.loc.start)
return element
}
function parseTag(context, type, parent) {
// 开始位置
const start = getCursor(context)
// 将匹配到 <xxx,若为自闭合,则匹配</xxx
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
// 标签名
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
// 根据开始标签进位
advanceBy(context, match[0].length)
// 根据空格进位,避免后续匹配首位为空格
advanceSpaces(context)
// 解析属性
let props = parseAttributes(context, type)
// 自闭合
let isSelfClosing = false
isSelfClosing = startsWith(context.source, '/>')
// 自闭合标签为>,普通标签为/>
advanceBy(context, isSelfClosing ? 2 : 1)
if (type === TagType.End) {
return
}
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
if (
// 查找template下是否存在if、else、else-if、for、slot的指令
props.some(
p => p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
) {
// 若有,则定义tagType为TEMPLATE,若没有则为默认的ELEMENT
tagType = ElementTypes.TEMPLATE
}
} else if (isComponent(tag, props, context)) {
tagType = ElementTypes.COMPONENT
}
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
}
function parseAttributes(
context: ParserContext,
type: TagType
): (AttributeNode | DirectiveNode)[] {
const props = []
const attributeNames = new Set<string>()
while (
context.source.length > 0 &&
!startsWith(context.source, '>') &&
!startsWith(context.source, '/>')
) {
const attr = parseAttribute(context, attributeNames)
if (
attr.type === NodeTypes.ATTRIBUTE &&
attr.value &&
attr.name === 'class'
) {
attr.value.content = attr.value.content.replace(/\s+/g, ' ').trim()
}
if (type === TagType.Start) {
props.push(attr)
}
if (/^[^\t\r\n\f />]/.test(context.source)) {
emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
}
advanceSpaces(context)
}
return props
}
function parseAttribute(context, nameSet) {
const start = getCursor(context)
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
// 属性名
const name = match[0]
nameSet.add(name)
// 属性名进位
advanceBy(context, name.length)
// 属性值
let value = undefined
if (/^[\t\r\n\f ]*=/.test(context.source)) {
// 等于符号前的空格进位
advanceSpaces(context)
// 等于符号进位 =
advanceBy(context, 1)
// 等于符号后的空格进位
advanceSpaces(context)
// 解析属性值
value = parseAttributeValue(context)
}
const loc = getSelection(context, start)
// 如果属性是以v-或:或.或@或#开头
if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
// 匹配出指令的具体名称
// v-if的名称是match的元素2为if,:value是match的元素2为value
// @click是match的元素3为click,#footer是match的元素3为footer
const match =
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
name
)!
// 若为.开头,则是缩写的属性
// .camel - 将短横线命名的 attribute 转变为驼峰式命名。
// .prop - 强制绑定为 DOM property。3.2+
// .attr - 强制绑定为 DOM attribute。3.2+
let isPropShorthand = startsWith(name, '.')
let dirName =
match[1] || // 若为指令或属性
(isPropShorthand || startsWith(name, ':')
? 'bind' // 属性的dirName为bind
: startsWith(name, '@')
? 'on' // 事件的dirName为on
: 'slot') // 插槽的dirName为slot
let arg: ExpressionNode | undefined
// 若为事件或插槽
if (match[2]) {
const isSlot = dirName === 'slot'
const startOffset = name.lastIndexOf(
match[2],
name.length - (match[3]?.length || 0)
)
const loc = getSelection(
context,
getNewPosition(context, start, startOffset),
getNewPosition(
context,
start,
startOffset + match[2].length + ((isSlot && match[3]) || '').length
)
)
let content = match[2]
let isStatic = true
// 若以[]包裹,则其属于动态插槽。如:<template #[slotName]>
if (content.startsWith('[')) {
isStatic = false
if (content.endsWith(']')) {
// 动态插槽的变量名
content = content.slice(1, content.length - 1)
}
}
arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY
: ConstantTypes.NOT_CONSTANT,
loc
}
}
// 否则作为属性和指令处理
// 若值被引号包裹
if (value && value.isQuoted) {
const valueLoc = value.loc
// 偏移'='的字符长度
valueLoc.start.offset++
valueLoc.start.column++
// 将属性值的结束位置更新
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
// 源码中移除引号
valueLoc.source = valueLoc.source.slice(1, -1)
}
// 若存在修饰符
const modifiers = match[3] ? match[3].slice(1).split('.') : []
if (isPropShorthand) modifiers.push('prop')
return {
type: NodeTypes.DIRECTIVE, // 所有事件、指令、插槽、动态属性都作为DIRECTIVE类型处理了
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT,
loc: value.loc
},
arg,
modifiers,
loc
}
}
return {
type: NodeTypes.ATTRIBUTE, // 静态属性才作为了ATTRIBUTE类型
name,
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
},
loc
}
}
function parseAttributeValue(context: ParserContext): AttributeValue {
const start = getCursor(context)
let content: string
const quote = context.source[0]
// 是否存在引号
const isQuoted = quote === `"` || quote === `'`
if (isQuoted) {
// 进位开始引号的字符长度
advanceBy(context, 1)
// 结束引号的位置
const endIndex = context.source.indexOf(quote)
if (endIndex === -1) {
content = parseTextData(
context,
context.source.length,
TextModes.ATTRIBUTE_VALUE
)
} else {
content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
// 进位结束引号的字符长度
advanceBy(context, 1)
}
} else {
// 无引号
const match = /^[^\t\r\n\f >]+/.exec(context.source)
if (!match) {
return undefined
}
content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
}
return { content, isQuoted, loc: getSelection(context, start) }
}
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# 3.3.4 parseText
该方法主要是用于解析文本,包括属性值、渲染文本、CDATA等。
function parseText(context: ParserContext, mode: TextModes): TextNode {
// 若模式是 CDATA 模式,则结束的符号为']]>',即标签内的文本作为纯文本处理,注意:html本身不支持 CDATA
// 否则应该是'<'和'{{',即下一个节点之前都属于纯文本
const endTokens = mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
// 源码最后的位置
let endIndex = context.source.length
for (let i = 0; i < endTokens.length; i++) {
// 查找结束符号在源码中的位置
const index = context.source.indexOf(endTokens[i], 1)
if (index !== -1 && endIndex > index) {
// 将结束符号的位置作为源码最后的位置
endIndex = index
}
}
const start = getCursor(context)
// 解析主要文本的内容
const content = parseTextData(context, endIndex, mode)
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start)
}
}
// 从当前位置获取给定长度的文本数据并反编码
function parseTextData(
context: ParserContext,
length: number,
mode: TextModes
): string {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
!rawText.includes('&')
) {
return rawText
} else {
// DATA 或 RCDATA 模式下包含&符号,需要反编码
return context.options.decodeEntities(
rawText,
mode === TextModes.ATTRIBUTE_VALUE
)
}
}
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
至此,parse 的源码分析暂时告一段落。
# 四、transform
所有的转换都是为后续的代码生成作准备。
# 4.1 主流程
export function transform(root: RootNode, options: TransformOptions) {
// 将所有属性挂载到 context 上
const context = createTransformContext(root, options)
// 遍历所有节点
traverseNode(root, context)
// 静态提升
if (options.hoistStatic) {
hoistStatic(root, context)
}
// 创建根节点代码生成调用
createRootCodegen(root, context)
// 最后确定的信息
root.helpers = new Set([...context.helpers.keys()])
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
// 遍历单个节点
export function traverseNode(node, context) {
context.currentNode = node
// 应用节点转换插件
const { nodeTransforms } = context
// 退出函数
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
// 执行转换,并拿到退出函数
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
}
switch (node.type) {
case NodeTypes.COMMENT:
context.helper(CREATE_COMMENT)
break
case NodeTypes.INTERPOLATION:
// 不需要遍历, 但是需要注入 toString 的帮助函数
// 插值节点在后续生成 render 代码的时候可以使用帮助函数获取变量的值
context.helper(TO_DISPLAY_STRING)
break
// 对于容器类型的节点, 需要进一步向下遍历
// IF 节点的子类放在 branches 中,单独处理
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
// 其他节点的子类放在 children 中
case NodeTypes.IF_BRANCH: // else-if else
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// 退出转换
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
// 创建根节点代码生成调用
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
// 根节点有多个子节点 - 返回一个fragment block.
let patchFlag = PatchFlags.STABLE_FRAGMENT
// codegenNode 为代码生成准备的
// 将调用挂载在节点的codegenNode上
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag + '',
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
}
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
87
88
89
90
91
92
93
94
95
96
97
# 4.2 对节点的转换
# 4.2.1 分类
# 4.2.1.1 transformOnce
对 v-once
的转换。
v-once: 仅渲染元素和组件一次,并跳过之后的更新。
# 4.2.1.2 transformIf
对 v-if
、v-else-if
、v-else
的转换。
# 4.2.1.3 transformMemo
对 v-memo
的转换。
v-memo: 允许传入一个固定长度的数组,当数组的每个值都与上一次渲染相同,则跳过更新。
# 4.2.1.4 transformFor
对 v-for
的转换。
# 4.2.1.5 transformSlotOutlet
对 v-slot
的转换。
v-once: 仅渲染元素和组件一次,并跳过之后的更新。
# 4.2.1.6 transformElement
对元素与组件的转换。
该类转换伴随对属性值的转换,buildProps就是在此处进行的。
# 4.2.1.7 trackSlotScopes
对 slot 的 scopes 的转换。
# 4.2.1.8 transformText
对v-text的转换。
# 4.2.2 实例分析
# 4.2.2.1 transformOnce
const seen = new WeakSet()
export const transformOnce: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
if (seen.has(node) || context.inVOnce || context.inSSR) {
return
}
seen.add(node)
// 添加vonce标识
context.inVOnce = true
context.helper(SET_BLOCK_TRACKING)
// 退出回调。将会在回调中会赋值 codegenNode
return () => {
context.inVOnce = false
const cur = context.currentNode
if (cur.codegenNode) {
cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */)
}
}
}
}
/*
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
}
*/
/**
* 查找 node 下的 props 中是否存在属性为 name 字符串或匹配规则满足 name 正则的属性
**/
export function findDir(node, name, allowEmpty) {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(allowEmpty || p.exp) &&
(isString(name) ? p.name === name : name.test(p.name))
) {
return p
}
}
}
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
# 4.2.2.2 transformElement
// 为元素的 codegen 生成一个 AST
export const transformElement: NodeTransform = (node, context) => {
// 处理并合并所有子表达式后,在退出时执行
return function postTransformElement() {
node = context.currentNode!
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
// 转换目标是创建一个继承了VNodeCall的codegenNode
let vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
const isDynamicComponent =
isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let vnodeDirectives: VNodeCall['directives']
if (props.length > 0) {
// 构建 props
const propsBuildResult = buildProps(node, context, undefined, isComponent, isDynamicComponent)
vnodeProps = propsBuildResult.props
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
const directives = propsBuildResult.directives
vnodeDirectives =
directives && directives.length
? createArrayExpression(directives.map(dir => buildDirectiveArgs(dir, context)))
: undefined
}
// 存在子节点
if (node.children.length > 0) {
const shouldBuildAsSlots = isComponent && vnodeTag !== TELEPORT && vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
// 构建插槽
const { slots, hasDynamicSlots } = buildSlots(node, context)
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
} else {
vnodeChildren = node.children
}
} else {
vnodeChildren = node.children
}
}
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
false /* disableTracking */,
isComponent,
node.loc
)
}
}
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
# 4.3 对指令的转换
# 4.3.1 分类
# 4.3.1.1 transformOn
对v-on的转换。
# 4.3.1.2 transformBind
对v-bind的转换。
# 4.3.1.3 transformModel
对v-model的转换。
# 4.3.2 buildProps
export function buildProps(node, context, props = node.props, isComponent, isDynamicComponent, ssr = false) {
const { tag, loc: elementLoc, children } = node
let properties = []
const mergeArgs = []
const runtimeDirectives = []
const hasChildren = children.length > 0
let shouldUseBlock = false
for (let i = 0; i < props.length; i++) {
// 静态属性
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop
let isStatic = true
if (name === 'ref') {
hasRef = true
if (context.scopes.vFor > 0) {
properties.push(
// 创建一个 AST 对象属性节点
createObjectProperty(
createSimpleExpression('ref_for', true),
createSimpleExpression('true')
)
)
}
}
properties.push(
createObjectProperty(
// 创建一个 AST 简单表达式节点
createSimpleExpression(
name,
true,
getInnerRange(loc, 0, name.length)
),
createSimpleExpression(
value ? value.content : '',
isStatic,
value ? value.loc : loc
)
)
)
} else {
// directives
const { name, arg, exp, loc, modifiers } = prop
/* 省略特殊处理 */
// 对指令的处理
const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) {
const { props, needRuntime } = directiveTransform(prop, node, context)
props.forEach(analyzePatchFlag)
}
}
}
/* 参数处理 */
return {
props: propsExpression,
directives: runtimeDirectives,
patchFlag,
dynamicPropNames
}
}
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
# 五、generate
# 5.1 主流程
export function generate(ast, options = {}) {
// 创建代码生成context
const context = createCodegenContext(ast, options)
const { mode, push, indent, deindent, newline } = context
const preambleContext = context
// 生成模块序文
genModulePreamble(ast, preambleContext, false, false)
// 进入渲染函数
const functionName = 'render'
const args = ['_ctx', '_cache']
const signature = args.join(', ')
push(`function ${functionName}(${signature}) {`)
indent()
// 生成组件资源声明
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
// 生成指令资源声明
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
// 临时变量导入
if (ast.temps > 0) {
// let _temp0, templ
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
// 生成 VNode 树表达式
push(`return `)
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: '',
map: context.map ? (context.map as any).toJSON() : undefined
}
}
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
# 5.2 导入生成
genModulePreamble
function genModulePreamble(ast, context, genScopeId) {
const { push, newline, runtimeModuleName } = context
if (genScopeId && ast.hoists.length) {
ast.helpers.add(PUSH_SCOPE_ID)
ast.helpers.add(POP_SCOPE_ID)
}
// 根据帮助函数生成导入声明
if (ast.helpers.size) {
const helpers = Array.from(ast.helpers)
// import { xxx } from "vue"
push(
`import { ${helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
if (ast.imports.length) {
// 生成外部依赖模块导入
genImports(ast.imports, context)
newline()
}
// 生成静态提升
genHoists(ast.hoists, context)
newline()
push(`export `)
}
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
// 生成资源解析声明
function genAssets(
assets: string[],
type: 'component' | 'directive' | 'filter',
{ helper, push, newline, isTS }: CodegenContext
) {
// 通过帮助函数生成了引入函数
const resolver = helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
let id = assets[i]
// 生成引入声明
// const xxx = components[id] / directive[id]
push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})${isTS ? `!` : ``}`
)
if (i < assets.length - 1) {
newline()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 5.2 节点生成
function genNode(node, context) {
// 纯字符串
if (isString(node)) {
context.push(node)
return
}
// symbol,通过帮助函数进行渲染
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
// 如果是容器节点,则再次调用genNode
case NodeTypes.ELEMENT: // children
case NodeTypes.IF: // 包含 v-if、v-else-if 与 v-else
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT: // 文本节点
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION: // 表达式节点
genExpression(node, context)
break
case NodeTypes.INTERPOLATION: // 插值节点
genInterpolation(node, context)
break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION: // 复合表达式节点
genCompoundExpression(node, context)
break
case NodeTypes.COMMENT: // 注释节点
genComment(node, context)
break
case NodeTypes.VNODE_CALL: // 单个节点
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION: // js调用表达式
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION: // js对象表达式
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION: // js数组表达式
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION: // js函数表达式
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION: // js条件表达式
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION: // js缓存表达式
genCacheExpression(node, context)
break
case NodeTypes.JS_BLOCK_STATEMENT: // js块声明
genNodeList(node.body, context, true, false)
break
case NodeTypes.IF_BRANCH:
break
default:
}
}
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
genVNodeCall
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent
} = node
if (directives) {
// 使用帮助函数生成添加指令函数
push(helper(WITH_DIRECTIVES) + `(`)
}
if (isBlock) {
// 帮助函数 OPEN_BLOCK 作用是会创建一个空数组,把后面的节点添加进来
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
}
if (pure) {
push(PURE_ANNOTATION)
}
// 创建 VNode 的调用
const callHelper: symbol = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
// 这里的 node 对应 setupBlock 的第一个参数
push(helper(callHelper) + `(`, node)
// 生成子节点
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
// 生成指令节点
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
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
# 5.2 静态提升
vue3的特性,会将静态元素和静态内容提升到 render 函数外
function genHoists(hoists, context) {
if (!hoists.length) {
return
}
context.pure = true
const { push, newline, helper, mode } = context
for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i]
if (exp) {
const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL
// const _hoisted_xxx =
push(
`const _hoisted_${i + 1} = `
)
// 具体节点或值
genNode(exp, context)
newline()
}
}
context.pure = false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.3 生成代码展示
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "纯文本", -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("template", null, _hoisted_2))
}
2
3
4
5
6
7
8
9
10
import { normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"
const _hoisted_1 = ["onClick"]
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("template", null, [
(_ctx.visible)
? (_openBlock(), _createElementBlock("div", {
key: 0,
class: _normalizeClass(_ctx.class),
onClick: _ctx.handleClick
}, "纯文本", 10 /* CLASS, PROPS */, _hoisted_1))
: _createCommentVNode("v-if", true)
]))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 六、总结
1、参与 vue 项目
2、借鉴 compile 思想
3、书写代码
4、其他