L
O
A
D
I
N
G

Vue2源码学习笔记(二)——编译模板原理

前言

Vue 2的模板编译原理具体可以分为以下几步:

  1. 模板解析:Vue的编译器会将模板字符串解析成抽象语法树(AST),这个AST表示了模板的结构和内容。
  2. 静态分析:编译器会对AST进行静态分析,检测模板中的指令、表达式和属性等,并生成相应的代码。
  3. 优化处理:编译器会对模板进行优化处理,包括静态节点的标记、静态属性的提取和静态文本的优化等。这些优化可以减少运行时的开销,提高渲染性能。
  4. 代码生成:根据AST生成可执行的渲染函数。渲染函数是一个JavaScript函数,它接收数据作为参数,并返回一个虚拟DOM节点。
  5. 渲染函数执行:在组件实例化或数据更新时,会调用渲染函数来生成虚拟DOM节点。渲染函数会根据数据的变化,生成新的虚拟DOM节点,并与旧的虚拟DOM节点进行比较,找出差异并更新实际的DOM。

通过模板编译,Vue将模板转换为可执行的渲染函数,这样在组件实例化和数据更新时,可以快速生成和更新虚拟DOM节点,从而实现高效的视图更新。编译过程中的优化处理可以减少不必要的计算和操作,提高渲染性能。同时,模板编译也提供了更简洁、易读和可维护的模板语法,使开发者能够更方便地编写和维护Vue组件的模板。

简单的例子看看图一乐吧:

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
// 假设有一个名为parseTemplate的函数,用于将模板解析成AST
function parseTemplate(template) {
// 解析模板,生成AST
// 省略具体实现
return ast;
}

// 假设有一个名为optimize的函数,用于优化AST
function optimize(ast) {
// 优化AST
// 省略具体实现
return optimizedAst;
}

// 假设有一个名为generateCode的函数,用于根据AST生成代码
function generateCode(ast) {
// 根据AST生成代码
// 省略具体实现
return code;
}

// 假设有一个名为compileTemplate的函数,用于编译模板
function compileTemplate(template) {
// 解析模板,生成AST
const ast = parseTemplate(template);

// 优化AST
const optimizedAst = optimize(ast);

// 根据AST生成代码
const code = generateCode(optimizedAst);

// 返回生成的代码
return code;
}

// 示例使用
const template = `
<div>
<h1>{{ title }}</h1>
<p v-if="showMessage">{{ message }}</p>
<button @click="handleClick">Click me</button>
</div>
`;

const compiledCode = compileTemplate(template);
console.log(compiledCode);

接下来我们看看具体室怎么实现的吧。

1.模板编译入口

上篇响应式原理中就说过中initMixin不仅仅会对初始化状态,还会在这个时候调用$mount方法进行挂载,vue也是在这个地方对模版进行编译的。

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
// src/init.js

import { initState } from "./state";
import { compileToFunctions } from "./compiler/index";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
// 这里的this代表调用_init方法的对象(实例对象)
// this.$options就是用户new Vue的时候传入的属性
vm.$options = options;
// 初始化状态
initState(vm);

// 如果有el属性 进行模板渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};

// 这块代码在源码里面的位置其实是放在entry-runtime-with-compiler.js里面
// 代表的是Vue源码里面包含了compile编译功能 这个和runtime-only版本需要区分开
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);

// 如果不存在render属性
if (!options.render) {
// 如果存在template属性
let template = options.template;

if (!template && el) {
// 如果不存在render和template 但是存在el属性 直接将模板赋值到el所在的外层html结构(就是el本身 并不是父元素)
template = el.outerHTML;
}

// 最终需要把tempalte模板转化成render函数
if (template) {
const render = compileToFunctions(template);
options.render = render;
}
}
};
}

首先需要对模板进行转化,其中核心方法就是compileToFunctions。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/compiler/index.js

import { parse } from "./parse";
import { generate } from "./codegen";
export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点
if (options.optimize !== false) {
optimize(ast, options);
}

// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}

2.模板解析

解析Html并生成ast

  • 首先需要各种规则匹配的正则表达式(开始标签,结束标签,花括号等)

  • createASTElement:将某一节点转为AST对象的函数

  • handleStartTag: 处理开始标签的函数

  • handleEndTag:处理结尾标签的函数

  • handleChars:处理文本节点的函数

  • 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
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
// src/compiler/parse.js

// 以下为源码的正则 对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性 形如 id="app"

let root, currentParent; //代表根节点 和当前父节点
// 栈结构 来表示开始和结束标签
let stack = [];
// 标识元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null,
};
}

// 对开始标签进行处理
function handleStartTag({ tagName, attrs }) {
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);
}

// 对结束标签进行处理
function handleEndTag(tagName) {
// 栈结构 []
// 比如 <div><span></span></div> 当遇到第一个结束标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来
let element = stack.pop();
// 当前父元素就是栈顶的上一个元素 在这里就类似div
currentParent = stack[stack.length - 1];
// 建立parent和children关系
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}

// 对文本进行处理
function handleChars(text) {
// 去掉空格
text = text.replace(/\s/g, "");
if (text) {
currentParent.children.push({
type: TEXT_TYPE,
text,
});
}
}

// 解析标签生成ast核心
export function parse(html) {
while (html) {
// 查找<
let textEnd = html.indexOf("<");
// 如果<在第一个 那么证明接下来就是一个标签 不管是开始还是结束标签
if (textEnd === 0) {
// 如果开始标签解析有结果
const startTagMatch = parseStartTag();
if (startTagMatch) {
// 把解析好的标签名和属性解析生成ast
handleStartTag(startTagMatch);
continue;
}

// 匹配结束标签</
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
handleEndTag(endTagMatch[1]);
continue;
}
}

let text;
// 形如 hello<div></div>
if (textEnd >= 0) {
// 获取文本
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
handleChars(text);
}
}

// 匹配开始标签
function parseStartTag() {
const start = html.match(startTagOpen);

if (start) {
const match = {
tagName: start[1],
attrs: [],
};
//匹配到了开始标签 就截取掉
advance(start[0].length);

// 开始匹配属性
// end代表结束符号> 如果不是匹配到了结束标签
// attr 表示匹配的属性
let end, attr;
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
advance(attr[0].length);
attr = {
name: attr[1],
value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值
};
match.attrs.push(attr);
}
if (end) {
// 代表一个标签匹配到结束的>了 代表开始标签解析完毕
advance(1);
return match;
}
}
}
//截取html字符串 每次匹配到了就往前继续匹配
function advance(n) {
html = html.substring(n);
}
// 返回生成的ast
return root;
}

3.静态分析和优化处理

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
 function optimize(ast,option) {
// 遍历AST的所有节点
traverse(ast, {
// 对于每个元素节点
ElementNode(node) {
// 检查节点是否有静态属性
if (hasStaticAttributes(node)) {
// 标记节点为静态节点
node.static = true;
}
},
// 对于每个文本节点
TextNode(node) {
// 检查节点是否为静态文本
if (isStaticText(node)) {
// 标记节点为静态节点
node.static = true;
}
}
});

return ast;
}

// 辅助函数:检查节点是否有静态属性
function hasStaticAttributes(node) {
// 检查节点的属性是否都是静态的
return node.attributes.every(attr => attr.static);
}

// 辅助函数:检查节点是否为静态文本
function isStaticText(node) {
// 检查文本节点的内容是否是静态的
return !/\{\{.*\}\}/.test(node.content);
}

// 辅助函数:遍历AST的所有节点
function traverse(ast, visitor) {
function traverseNode(node) {
// 调用对应节点类型的处理函数
const handler = visitor[node.type];
if (handler) {
handler(node);
}

// 递归遍历子节点
if (node.children) {
node.children.forEach(traverseNode);
}
}

traverseNode(ast);
}

// 示例使用
const ast = {
type: 'ElementNode',
tag: 'div',
attributes: [
{ name: 'class', value: 'container', static: true },
{ name: 'id', value: 'app', static: true }
],
children: [
{
type: 'TextNode',
content: 'Hello, world!',
static: true
}
]
};

const optimizedAst = optimize(ast);
console.log(optimizedAst);

4.代码生成(将AST转换成render函数格式的数据)

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
// src/compiler/gen.js

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{ }} 捕获花括号里面的内容

function gen(node) {
if (node.type === 1) {
// 元素节点处理
return generate(node)
} else {
// 文本节点处理
const text = node.text

// 检测是否有花括号{{}}
if (!defaultTagRE.test(text)) {
// 没有的话直接返回 _v,创建文本节点
return `_v(${JSON.stringify(text)})`
}


// 每次赋值完要重置defaultTagRE.lastIndex
// 因为正则规则加上全局g的话,lastIndex会逐步递增,具体可以百度查一查正则的全局g情况下的test方法执行后的lastIndex
let lastIndex = (defaultTagRE.lastIndex = 0);
const tokens = []
let match, index

while ((match = defaultTagRE.exec(text))) {
// 文本里只要还存在{{}}就会一直正则匹配
index = match.index
if (index > lastIndex) {
// 截取{{xxx}}中的文本xxx
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}

tokens.push(`_s(${match[1].trim()})`)


// 推进lastIndex
lastIndex = index + match[0].length

}

// 匹配完{{}}了,但是还有剩余的文本,那就还是push进去
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}

// return _v函数创建文本节点
return `_v(${tokens.join('+')})`
}

}


// 生成render函数格式的code的函数
function generate(el) {
const children = getChildren(el)
const code = `_c('${el.tag}',${el.attrs.length ? `${genProps(el.attrs)}` : "undefined"
}${children ? `,${children}` : ""})`;;
return code
}

// 处理attrs的函数
function genProps(attrs) {
let str = ''
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]

if (attr.name === 'style') {
const obj = {}

attr.value.split(';').forEach(item => {
const [key, value] = item.split(':')
obj[key] = value
})
attr.value = obj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, str.length)}}`
}

// 获取子节点,进行gen的递归
function getChildren(el) {
const children = el.children
if (children && children.length) {
return `${children.map(c => gen(c)).join(',')}`
}
}

module.exports = {
generate
}

5.思维导图

image.png

参考文章

手写Vue2.0源码(二)-模板编译原理|技术点评 - 掘金 (juejin.cn)

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

我是穷比,在线乞讨!

支付宝
微信