0%

vue源码学习-模板编译原理


由于之前发生了一些事情,并且公司的项目也很赶,碰巧孩子出生了,所以忙的不可开交,,很久没有更新博客了,今天有时间,把欠下的博客都补上

我们在学习Vue的时候,一直都说虚拟DOM,虚拟DOM,那么,什么是虚拟DOM,虚拟DOM又是怎么生成的呢?

虚拟DOM的生成大致上是分为这么几步的:

  1. template 通过正则转化为ast树

  2. ast树通过codegen方法,转化为render函数

  3. render函数,内部调用_c方法,( _c方法就是创建el) 转化为虚拟dom

用一个对象来描述一个DOM元素,这就是虚拟DOM

知道了大致的流程,我们再去源码中看看具体的代码是怎么实现的(源码的位置在scr/compiler/index.js中)

首先,创建了一个编译器(createCompilerCreator),传入了一个 bsaeCompile函数,这个函数描述了模板是怎么转化成render函数的

1
2
3
4
5
6
7
8
9
10
11
12
function baseCompile(template: string, options: CompilerOptions) {
const ast = parse(template.trim(), options) // 1.将模板转化成ast语法树  
if (options.optimize !== false) { // 2.优化树    
optimize(ast, options)
}
const code = generate(ast, options) // 3.生成树  
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

Vue的源码看起来有点乱,上百度找了大神整理好的,看看bsaeCompile展开后,还做了哪些操作

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
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是 标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+| ([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
let root;
let currentParent;
let stack = []

function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: 1,
children: [],
attrs,
parent: null
}
}

function start(tagName, attrs) {
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);
}

function chars(text) {
currentParent.children.push({
type: 3,
text
})
}

function end(tagName) {
const element = stack[stack.length - 1];
stack.length--;
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element)
}
}

function parseHTML(html) {
while (html) {
let textEnd = html.indexOf('<');
if (textEnd == 0) {
const startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1])
}
}
let text;
if (textEnd >= 0) {
text = html.substring(0, textEnd)
}
if (text) {
advance(text.length);
chars(text);
}
}

function advance(n) {
html = html.substring(n);
}

function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length);
let attr, end
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
match.attrs.push({
name: attr[1],
value: attr[3]
})
}
if (end) {
advance(end[0].length);
return match
}
}
}
} // 生成语法树
parseHTML(`<div id="container"><p>hello<span>zf</span></p></div>`);

function gen(node) {
if (node.type == 1) {
return generate(node);
} else {
return `_v(${JSON.stringify(node.text)})`
}
}

function genChildren(el) {
const children = el.children;
if (el.children) {
return `[${children.map(c=>gen(c)).join(',')}]`
} else {
return false;
}
}

function genProps(attrs) {
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
str += `${attr.name}:${attr.value},`;
}
return `{attrs:{${str.slice(0,-1)}}}`
}

function generate(el) {
let children = genChildren(el);
let code = `_c('${el.tag}'${el.attrs.length?`,${genProps(el.attrs)}`:''}${children? `,${children}`:''})`;
return code;
} // 根据语法树生成新的代码
let code = generate(root);
let render = `with(this){return ${code}}`;
// 包装成函数
let renderFn = new Function(render);
console.log(renderFn.toString());

假设我们的template里面写了这个 <div id="container"><p>hello<span>zf</span></p></div>

通过parseHTML函数,循环遍历里面的每一个字符串,判断是否为’<’

每次循环判断完之后,都会删除对应的长度

匹配到了<div后,生成一个对象,将’div’放入了tagName中,可能div会有属性,也生成一个attrs

1
2
3
4
5
<!-- 就是上面代码中的这一段 -->
const match = {
tagName: start[1],
attrs: []
}

之后会继续循环匹配,看看是否是 ‘>’ 关闭标签,如果不是的话,继续循环

循环到了是一个 id="container",根据正则来判断是否是属性,放入了attrs

再次循环匹配到是一个 ‘>’ 关闭标签,返回这个对象

这时候字符串已经剩下这样的了 <p>hello<span>zf</span></p></div>, 前面的<div id="container"> 都已经判断处理完了

再一次判断到是’<’后,重复上面的操作

不一样的是,p标签中是一个’hello’字符串,通过正则判断后,是将这个hello传入chars中,代码中也有,就不写出来了

通过这样一步一步的循环遍历判断,最终会生成一个完整的对象

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
{
tag: "div"
type: 1,
children: [{
tag: "p"
type: 1,
children: [{
type: 3
text: "hello"
},
{
tag: "span"
type: 1,
children: [{
type: 3
text: "zf"
}],
attrs: [],
parent: '' // 父级
}],
attrs: [],
parent: '' // 父级
}],
attrs: [{
name: "id"
value: "container"
}],
parent: null
}

这样就可以用一个对象来描述一个DOM元素

以上就是我对vue模板编译的一些理解,如果文章由于我学识浅薄,导致您发现有严重谬误的地方,请一定在评论中指出,我会在第一时间修正我的博文,以避免误人子弟。

-------------本文结束感谢您的阅读-------------
没办法,总要恰饭的嘛~~