# Vue 模板编译源码解析
TIP
此篇主要讲了根据传入的元素或者模板(template:'<div id="a"></div>'),拿到html字符串,再根据正则将html字符串编译成ast对象,再由ast对象转化为code,最后采用new Function + with的方式根据code生成render函数的过程
根据el或template拿到 HTML字符串 -> 将HTML字符串转化为ast对象 -> 根据ast对象生成code -> 用code生成render函数
<div id="app">
hello {{ name }} world
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
name: 'mrzhao',
},
// render(h) {
// return h('div',{id:'a'},'mrzhao')
// },
// template:`<div id="a">{{name}}</div>`
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TIP
对于传入的el或者template属性,最后都会被解析成render函数,以便后面更新视图。
# 处理 render 方法
// src/init.js
import { initState } from "./state";
import { compileToFunction } from "./compiler/index";
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
// el,data
const vm = this;
vm.$options = options; // 后面会对options进行扩展操作
// 对数据进行初始化 watch computed props data ...
initState(vm); // vm.$options.data 数据劫持
// 如果有el元素,将数据渲染到模板上
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
Vue.prototype.$mount = function(el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
vm.$el = el;
// 把模板转化成 对应的渲染函数(render) =》 虚拟dom概念 vnode =》 diff算法 更新虚拟dom =》 产生真实节点,更新
// 如果有render 就用render
// 没有render 看有没有template 有就用
// 没有template 就找el
if (!options.render) {
// 没有render用template,目前没render
let template = options.template;
if (!template && el) {
// 用户也没有传递template 就取el的内容作为模板
template = el.outerHTML;
}
// 最后需要把template转换成render函数
let render = compileToFunction(template);
// 生成render函数后挂载到vm的options属性上
options.render = render;
}
// options.render 就是渲染函数
// 调用render方法 渲染成真实dom 替换掉页面的内容
};
}
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
render
initMixin中会集中对el属性和template属性做处理,统一处理成render函数,方便后续更新视图时直接调用生成真实 DOM,替换页面的内容
# 核心方法 compileToFunction
compileToFunction
compileToFunction方法是将模板转化成render函数的核心方法
// src/compiler/index.js
import { generate } from "./generate";
import { parserHTML } from "./parser";
export function compileToFunction(template) {
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 语法不存在的属性无法描述
let ast = parserHTML(template);
// 拿到ast对象生成code
let code = generate(ast);
let render = new Function(`with(this){return ${code}}`); // code 中会用到数据 数据在vm上
return render;
// html=> ast(只能描述语法 语法不存在的属性无法描述) => render函数 + (with + new Function) => 虚拟dom (增加额外的属性) => 生成真实dom
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
compileToFunction
compileToFunction是编译的核心方法,会先将html字符串转化为ast语法树,然后根据ast生成render函数
# parserHTML(将 HTML 转换成 ast 语法树)
// src/compiler/parser.js
// 匹配HTML中内容的正则
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // 用来获取的标签名的 match后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签的开始
const startTagClose = /^\s*(\/?)>/; // 匹配标签的结束 /> <div/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配闭合标签的
// 匹配属性 a=b a="b" a='b'
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 将我们的html =》 词法解析 (开始标签 , 结束标签,属性,文本)
// 将解析后的结果 组装成一个树结构 栈
function createAstElement(tagName, attrs) {
return {
tag: tagName,
type: 1, // 1表示元素,3表示文本
children: [],
parent: null,
attrs,
};
}
// 开始标签
function start(tagName, attributes) {
// 在遇到新的开始标签时,栈中的最后一个标签就是当前开始标签的父元素
let parent = stack[stack.length - 1];
let element = createAstElement(tagName, attributes);
if (!root) {
root = element;
}
if (parent) {
element.parent = parent; // 当放入栈中时 继续父亲是谁
parent.children.push(element);
}
stack.push(element);
}
// 闭合标签
function end(tagName) {
// 遇到闭合标签就把与之对应的开始标签从栈中弹出
let last = stack.pop();
// 如果弹出的标签名与当前匹配的闭合标签不匹配,表示标签出错了
if (last.tag !== tagName) {
throw new Error("标签有误");
}
}
// 处理文本
function chars(text) {
// 去掉空格
text = text.replace(/\s/g, "");
let parent = stack[stack.length - 1];
if (text) {
parent.children.push({
type: 3, // 文本类型为3
text,
});
}
}
export function parserHTML(html) {
function advance(len) {
html = html.substring(len);
}
let root = null;
// 采用栈结构存放遇到的标签,1、为了拿到父标签。2、验证标签是否匹配
let stack = [];
// 匹配开始标签并解析属性
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: [],
};
// 删掉解析完的字符
advance(start[0].length);
let end;
// 如果没有遇到标签结尾就不停的解析
let attr;
// 如果没有匹配到标签结尾(>) 并且 匹配到了属性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5],
});
advance(attr[0].length);
}
if (end) {
advance(end[0].length);
}
return match;
}
return false; // 不是开始标签
}
while (html) { // 解析到没有内容为止
let textEnd = html.indexOf("<");
// 如果<在第一个 那么证明接下来可能是标签(开始或结束标签),也可能是文本符号
if (textEnd == 0) {
// 解析开始标签
const startTagMatch = parseStartTag(html);
// 是开始标签
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
// 解析结束标签
const endTagMatch = html.match(endTag);
// 是结束标签
if (endTagMatch) {
end(endTagMatch[1]);
advance(endTagMatch[0].length);
continue;
}
}
let text; // {{name}} world</div>
// <大于0代表有文本 解析文本
if (textEnd > 0) {
text = html.substring(0, textEnd);
}
if (text) {
// 处理文本
chars(text);
advance(text.length);
}
}
return root;
}
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
parserHTML
主要解析HTML的方法,采用解析完一部分就删除的规则,正则匹配的方式,解析HTML中的标签、标签属性、文本,并建立父子关系,最终生成ast元素对象 { tag:'div',type:1,children:[{ type:3,text:'hello world'}], parent:undefined,attrs: [{name:'id',value:'app'}]}
# 将ast元素对象转化为代码
// src/compiler/generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配带有大括号的内容 {{aaaaa}}
// { tag:'div',type:1,children:[{ type:3,text:'hello {{name}} world'}], parent:undefined,attrs: [{name:'id',value:'app'}]} =》 字符串 _c('div',{id:'app'},_v('hello' + _s(name) + 'world'))
// 循环属性生成 属性 code
function genProps(attrs) {
// [{name:'xxx',value:'xxx'},{name:'xxx',value:'xxx'}]
let str = "";
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
// 样式需要单独处理
if (attr.name === "style") {
// color:red;background:blue
let styleObj = {};
attr.value.replace(/([^;:]+)\:([^;:]+)/g, function () {
styleObj[arguments[1]] = arguments[2];
});
attr.value = styleObj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
function gen(el) {
// 判断节点类型 1:元素节点 3:文本节点
if (el.type == 1) {
// 递归生成元素code
return generate(el);
} else {
let text = el.text;
// 没有双括号直接当文本处理
if (!defaultTagRE.test(text)) {
return `_v('${text}')`;
} else {
// 存在双括号
// 'hello' + name + 'world' hello {{name}} world
let tokens = [];
let match;
// exec匹配时对于带有全局修饰符g的,第一次匹配到时,下次再匹配时是从上次匹配到的值索引之后开始匹配
// 由于我们每次匹配都是用的共用的正则 defaultTagRE,所以每次调用gen 需要处理{{}}时都需要重置 lastIndex
let lastIndex = (defaultTagRE.lastIndex = 0); // CSS-LOADER 原理一样
// 如果没有匹配到,那么match 为 null
while ((match = defaultTagRE.exec(text))) {
// 匹配到的值所在的索引
let index = match.index;
if (index > lastIndex) {
// 将字符串开头到 {{}} 之前的字符 截取放入tokens
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`); // 拿到{{ }}中的内容 name
// 更新索引
lastIndex = index + match[0].length;
}
// 当匹配不到{{}}时,并且后面还有字符时,将剩余的字符直接
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join("+")})`;
}
}
}
// 循环子节点生成code
function genChildren(el) {
let children = el.children; // 获取儿子
if (children) {
return children.map(c => gen(c)).join(",");
}
return false;
}
// 递归生成code: _c('div',{id:'app'},_v('hello' + _s(name) + 'world'))
export function generate(el) {
// 遍历树 将树拼接成字符串
let children = genChildren(el);
let code = `_c('${el.tag}',${
el.attrs.length ? genProps(el.attrs) : "undefined"
}${children ? `,${children}` : ""})`;
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
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
生成code
拿到生成的ast对象,ast对象转化成类似_c('div',{id:'app'},_v('hello' + _s(name) + 'world'))这样的字符串
# 拿到code生成render函数
// src/compiler/index.js
import { generate } from "./generate";
import { parserHTML } from "./parser";
export function compileToFunction(template) {
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 语法不存在的属性无法描述
// let ast = parserHTML(template);
// 拿到ast对象生成code
let code = generate(ast);
// 模板引擎基本用的都是 new Function + with的方式将字符串转换成函数
// 使用with语法改变作用域中的默认对象为this,后续所有的引用都指向this对象,会去this上找对应的属性,不用添加命名空间, 之后调用render函数可以使用call改变this 方便code里面的变量取值 比如 name值就变成了this.name
let render = new Function(`with(this){return ${code}}`); // code 中会用到数据 数据在vm上
// render.call(vm) 相当于 vm.name
return render;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20