1. 概念
模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?这就归功于模板编译功能。
Vue 的模板编译是在 mount 的过程中进行的,在 mount的时候执行了 compile 这个方法来将 template 里的内容转换成真正的 HTML 代码。complie 最终生成 render 函数,等待调用。
模板编译的作用是生成渲染函数,通过执行渲染函数生成最新的vnode,最后根据vnode进行渲染。
2.将模板编译成渲染函数
2.1 两个步骤,三部分内容
两个步骤:
- 模板解析成AST(Abstract Syntax Tree,抽象语法树,用对象描述节点)
- 使用AST生成渲染函数
*** 由于静态节点不需要总是重新渲染,所以在生成AST之后,生成渲染函数之前,需要遍历AST标记静态节点
三部分内容分别抽象出三个模块:
- 解析器:模板解析成AST
- 优化器:遍历AST标记静态节点
- 代码生成器:使用AST生成渲染函数
2.2 解析器
2.2.1 解析器的作用
将模板字符串解析成AST
<div>
<p>{{name}}</p>
</div>
上述模板转化成AST后
{
tag: "div",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [{
tag: "p",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag:"div", ... },
attrsList: [],
attrsMap: {},
children: [{
type:2,
text:"{{name}}",
static:false,
expression:"_s(name)"
}]
}]
}
2.2.2 解析器内部运行原理
解析器包括:HTML解析器,文本解析器,过滤解析器
HTML解析器:HTML解析器的作用是解析HTML,它在解析的过程中会触发不同的钩子函数。这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数
2.2.2.1 钩子函数
伪代码如下:
parseHTML(template,{
start(start){
// 每当解析到标签的开始位置时,触发该函数
},
end(){
// 每当解析到标签的结束位置时,触发该函数
},
chars(text){
// 每当解析到文本时,触发改函数
},
comment(text){
// 每当解析到注释时,触发该函数
}
})
例:
<div>
<p>{{name}}</p>
</div>
触发钩子函数的顺序是:start(),start(),chars(),end(),end()
2.2.1.2 层级关系
用栈来记录层级关系
触发start函数-->将节点推入栈中
触发end函数-->将节点弹出栈中
<div>
<p>
<span>栈</span>层级关系</p>
</div>
对于当前节点span来说:
| 根节点 | 父节点 |
|---|---|
| div | p |
2.3 HTML解析器
解析HTML模板的过程就是循环的过程
代码查看解析过程
// 匹配开始标签的开头<
const tagStartReg = /^<([a-zA-Z_][\w\-]*)/;
// 匹配开始标签的结尾,是否是自闭和标签 />
const tagStartCloseReg = /^\s*(\/?)>/;
// 匹配标签的属性
const tagAttrReg = /^\s*([a-zA-Z_@:][\w\-\:]*)(?:(?:=)(?:(?:"([^"]*)")|(?:'([^']*)')))?/;
// 匹配结束标签 </as>
const tagEndReg = /^<\/([a-zA-Z_][\w\-]*)>/;
// 匹配文本,不是以<开头
const textReg = /^[^<]*/;
// 全局匹配 {{}} 不是{}的字符
const dataReg = /\{\{([^{}]+)\}\}/g;
let stack = [];
let currentAst = {};
let root = null;
let index = 0; // 模板解析指针位置
let template = `<div id="index"><p>hello, {{msg}}</p>vue编译</div>`
parseHTML(template, {
start,
end,
chars
});
// template to ast
function parseHTML(templates, hooks) {
template = templates
while (template) {
let tagStart = template.indexOf('<');
if (tagStart === 0) {
let start = template.match(tagStartReg); // ["<div", "div", index: 0, input: "<div id="index"><p>hello, {{msg}}</p>vue编译</div>", groups: undefined]
if (start) {
hooks.start(start);
};
let end = template.match(tagEndReg);
if (end) {
step(end[0].length);
// 结束标签,触发结束钩子函数
hooks.end();
};
} else {
let text = template.match(textReg);
step(text[0].length);
// 文本标签,触发文本钩子函数
let ast = {
type: 'text',
value: hooks.chars(text[0].replace(/\s+/, ' '))
}
currentAst.children.push(ast);
ast.parent = currentAst;
}
}
console.log('root', root);
return root;
}
// 解析到开始标签的位置时,触发的钩子函数
function start(start) {
// 是开始标签,触发钩子函数start
let ast = {
type: 'tag',
name: start[1],
attrs: [],
children: []
};
// 指针向后移动
step(start[0].length);
// 匹配属性
let end, attr;
while (!(end = template.match(tagStartCloseReg)) && (attr = template.match(tagAttrReg))) {
ast.attrs.push({
key: attr[1],
value: attr[2] || attr[3] // 双引号时是attr[2],单引号时是attr[3]
});
step(attr[0].length);
}
if (end) {
step(end[0].length);
// 不是自闭和标签
if (!end[1]) {
if (!root) {
root = ast;
} else {
currentAst.children.push(ast);
ast.parent = currentAst;
}
stack.push(ast);
currentAst = ast;
}
};
}
// 解析到结束标签的位置时,触发的钩子函数
function end() {
// 结束标签弹出栈中
stack.pop();
currentAst = stack[stack.length - 1];
}
// 解析到文本的位置时,触发的钩子函数
function chars(text) {
const result = []
let lastIndex = 0
let match, index
// 是否存在变量{{}}
while ((match = dataReg.exec(text))) {
index = match.index
if (index > lastIndex) {
result.push(JSON.stringify(text.slice(lastIndex, index)))
}
let data = match[1].trim()
result.push(`${data === null ? '' : data}`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
result.push(JSON.stringify(text.slice(lastIndex)))
}
return result.join('+')
}
// 截取模板
function step(length) {
index += length;
template = template.slice(length); // 'id="index"><p>hello, {{msg}}</p>vue编译</div>'
}