前端编译原理-VUE模板编译概览

  目录

VUE模板编译工作流程简介

概要

VUE的模板是要被转换成js渲染函数,在运行时执行的。

主要流程:

模板->parse(str)->模板AST->transform(ast)->javascript AST->generate(JSAST)->渲染函数

模板

1
<div><p>Vue</p><p>Template</p></div>

parse

词法分析

词法分析是把字符串解析成tokens

模板字符串转化后的tokens:

1
2
3
4
5
6
7
8
9
10
[
{ type: 'tag', name: 'div' },
{ type: 'tag', name: 'p' },
{ type: 'text', content: 'Vue' },
{ type: 'tagEnd', name: 'p' },
{ type: 'tag', name: 'p' },
{ type: 'text', content: 'Template' },
{ type: 'tagEnd', name: 'p' },
{ type: 'tagEnd', name: 'div' }
]

实现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
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
const State = {
initial: 1,
tagOpen: 2,
tagName: 3,
text: 4,
tagEnd: 5,
tagEndName: 6
}

function isAlpha(char) {
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}

function tokenize(str) {
let currentState = State.initial
const chars = []
const tokens = []
while(str) {
const char = str[0]
switch (currentState) {
case State.initial:
if (char === '<') {
currentState = State.tagOpen
str = str.slice(1)
} else if (isAlpha(char)) {
currentState = State.text
chars.push(char)
str = str.slice(1)
}
break
case State.tagOpen:
if (isAlpha(char)) {
currentState = State.tagName
chars.push(char)
str = str.slice(1)
} else if (char === '/') {
currentState = State.tagEnd
str = str.slice(1)
}
break
case State.tagName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tag',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.text:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '<') {
currentState = State.tagOpen
tokens.push({
type: 'text',
content: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
case State.tagEnd:
if (isAlpha(char)) {
currentState = State.tagEndName
chars.push(char)
str = str.slice(1)
}
break
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char)
str = str.slice(1)
} else if (char === '>') {
currentState = State.initial
tokens.push({
type: 'tagEnd',
name: chars.join('')
})
chars.length = 0
str = str.slice(1)
}
break
}
}

return tokens
}

语法分析

语法分析是把词法分析出来的tokens解析成AST

模板AST:
⬇⬇⬇
实现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
function parse(str) {
const tokens = tokenize(str)

const root = {
type: 'Root',
children: []
}
const elementStack = [root]

while (tokens.length) {
const parent = elementStack[elementStack.length - 1]
const t = tokens[0]
switch (t.type) {
case 'tag':
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
parent.children.push(elementNode)
elementStack.push(elementNode)
break
case 'text':
const textNode = {
type: 'Text',
content: t.content
}
parent.children.push(textNode)
break
case 'tagEnd':
elementStack.pop()
break
}
tokens.shift()
}

return root
}

模板AST

通过parse步骤后得到的模板AST:

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
{
"type": "Root",
"children": [
{
"type": "Element",
"tag": "div",
"children": [
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "Vue"
}
]
},
{
"type": "Element",
"tag": "p",
"children": [
{
"type": "Text",
"content": "Template"
}
]
}
]
}
]
}

transform

将模板AST转换成javascript AST,此处使用了插件结构,处理转换节点的函数是单独出来的

主函数

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
function transform(ast) {
const context = {
currentNode: null,
parent: null,
replaceNode(node) {
context.currentNode = node
context.parent.children[context.childIndex] = node
},
removeNode() {
if (context.parent) {
context.parent.children.splice(context.childIndex, 1)
context.currentNode = null
}
},
nodeTransforms: [
transformRoot,
transformElement,
transformText
]
}
// 调用 traverseNode 完成转换
traverseNode(ast, context)
}

function traverseNode(ast, context) {
context.currentNode = ast

const exitFns = []
const transforms = context.nodeTransforms
for (let i = 0; i < transforms.length; i++) {
const onExit = transforms[i](context.currentNode, context)
if (onExit) {
exitFns.push(onExit)
}
if (!context.currentNode) return
}

const children = context.currentNode.children
if (children) {
for (let i = 0; i < children.length; i++) {
context.parent = context.currentNode
context.childIndex = i
traverseNode(children[i], context)
}
}

let i = exitFns.length
while (i--) {
exitFns[i]()
}
}

插件工具函数

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
function transformText(node) {
if (node.type !== 'Text') {
return
}

node.jsNode = createStringLiteral(node.content)
}


function transformElement(node) {

return () => {
if (node.type !== 'Element') {
return
}

const callExp = createCallExpression('h', [
createStringLiteral(node.tag)
])
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push(
createArrayExpression(node.children.map(c => c.jsNode))
)

node.jsNode = callExp
}
}

function transformRoot(node) {
return () => {
if (node.type !== 'Root') {
return
}

const vnodeJSAST = node.children[0].jsNode

node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST
}
]
}
}
}

function createStringLiteral(value) {
return {
type: 'StringLiteral',
value
}
}

function createIdentifier(name) {
return {
type: 'Identifier',
name
}
}

function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements
}
}

function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments
}
}

javascript AST

模板字符串经过转换后生成的javascript AST:

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
{
"type": "FunctionDecl",
"id": {
"type": "Identifier",
"name": "render"
},
"params": [],
"body": [
{
"type": "ReturnStatement",
"return": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "div"
},
{
"type": "ArrayExpression",
"elements": [
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "p"
},
{
"type": "StringLiteral",
"value": "Vue"
}
]
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "h"
},
"arguments": [
{
"type": "StringLiteral",
"value": "p"
},
{
"type": "StringLiteral",
"value": "Template"
}
]
}
]
}
]
}
}
]
}

generate

得到转换后的javascript AST后,生成渲染函数字符串

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
87
88
89
90
91
92
93
94
95
96
97
98
99
function generate(node) {
const context = {
code: '',
push(code) {
context.code += code
},
currentIndent: 0,
newline() {
context.code += '\n' + ` `.repeat(context.currentIndent)
},
indent() {
context.currentIndent++
context.newline()
},
deIndent() {
context.currentIndent--
context.newline()
}
}

genNode(node, context)

return context.code
}

function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl':
genFunctionDecl(node, context)
break
case 'ReturnStatement':
genReturnStatement(node, context)
break
case 'CallExpression':
genCallExpression(node, context)
break
case 'StringLiteral':
genStringLiteral(node, context)
break
case 'ArrayExpression':
genArrayExpression(node, context)
break
}
}

function genFunctionDecl(node, context) {
const { push, indent, deIndent } = context

push(`function ${node.id.name} `)
push(`(`)
genNodeList(node.params, context)
push(`) `)
push(`{`)
indent()

node.body.forEach(n => genNode(n, context))

deIndent()
push(`}`)
}

function genNodeList(nodes, context) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
genNode(node, context)
if (i < nodes.length - 1) {
push(', ')
}
}
}

function genReturnStatement(node, context) {
const { push } = context

push(`return `)
genNode(node.return, context)
}

function genCallExpression(node, context) {
const { push } = context
const { callee, arguments: args } = node
push(`${callee.name}(`)
genNodeList(args, context)
push(`)`)
}

function genStringLiteral(node, context) {
const { push } = context

push(`'${node.value}'`)
}

function genArrayExpression(node, context) {
const { push } = context
push('[')
genNodeList(node.elements, context)
push(']')
}

渲染函数

经过transform转换后生成的渲染函数字符串:

1
2
3
function render () {
return h('div', [h('p', 'Vue'), h('p', 'Template')])
}

总结

最后总结代码:

1
2
3
4
5
6
7
8
9
10
function compile(template) {
// 模板AST
const ast = parse(template);
// 将模板AST转换为javascript AST
transform(ast);
// 代码生成
const code = generate(ast.jsNode);

return code;
}

本篇内容摘取《VUE设计与实现》的第15章,简单的介绍了前端模板的编译解析步骤。
本篇code
VUE设计与实现code