template模板是怎么通过Compile编译的
我们回头再来看Vue原理学习(一)中的这张图:
对于这张图,我们这次来研究complited到render function这一部分。
另外,我们同样再来看下面一段Vue代码:
<template>
<div>hello</div>
</template>
这段Vue代码,使用了template语法,而我们本次讨论的就是 template模板是怎么通过Compile编译的?
前面我们也已经简单的提到过,Vue通过正则解析(parse)、优化(optimize),最终生成(generate)AST,转成render function。
而这里,我们在详细的介绍下。
准备一个示例
首先我们准备一个示例,围绕这个示例来进行探讨学习。
<div :class="c" class="demo" v-if="isShow">
<span v-for="item in sz">{{item}}</span>
</div>
字符串形式:
const html = '<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz">{{item}}</span></div>';
对于AST,我们可以先看下究竟长什么样子。
{
// 标签属性map,记录标签上都有哪些属性
'attrsMap': {
':class': c,
'class': 'demo',
'v-if': 'isShow'
},
// 解析得到的:class
‘classBinding’: 'c',
// v-if 解析
'if': 'isShow',
// v-if 的条件
'ifConditions': [
{
'exp': 'isShow'
}
],
// 标签属性class,非绑定class
'staticClass': 'demo',
// 标签tag
'tag': 'div',
// 子节点数据
'children': [
{
'attrsMap': {
'v-for': "item in sz"
},
// for 循环参数,alis:循环项的别名, 也就是 xx in data 中的 xx
'alias': 'item',
// for 循环对象
'for': 'sz',
// for 循环是否已被处理标记
'forProcessed': true,
'tag': 'span',
'children': [
{
/* 表达式,_s是一个转字符串的函数 */
'expression': '_s(item)',
'text': '{{item}}'
}
]
}
]
}
你会发现,和Virtual DOM 有相似的感觉,但是不要混淆,其作用不同。另外,如果你仔细的阅读Vue源码,会发现,上述代码描述,在源码中都存在,包括下面介绍的正则代码。
定义正则
前面提到了,parse的过程,就是用到了正则表达式进行检索查询处理。
const ncname = '[a-zA-Z_][\\w\\-\\.]*';
const singleAttrIdentifier = /([^\s"'<>/=]+)/
const singleAttrAssign = /(?:=)/
const singleAttrValues = [
/"([^"]*)"+/.source,
/'([^']*)'+/.source,
/([^\s"'=<>`]+)/.source
]
const attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
)
const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'
const startTagOpen = new RegExp('^<' + qnameCapture)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>')
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
advance
这里我们介绍下,由于解析template采用的是循环进行字符串匹配的方式,也就是我们每次匹配解析完一段代码后,都要将已经匹配的代码去掉,头部指针重新指向接下来需要匹配的部分,直到没有可匹配项。
advance
方法在Vue源码中存在定义,大概在8861行。
// 参数n就是每次匹配的位置,或者说每次匹配到的那段代码的长度
function advance(n) {
index += n;
html = html.substring(n);
}
当我们匹配完<div :class="c" class="demo" v-if="isShow">
这段代代码后,指针将移动43个字符(因为长度等于43),此时,调用advancef方法:
advance(43)
parseHTML
parseHTML
方法在Vue源码中也存在定义,大概在8734行。这里我们对其做了简化。
function parseHTML(html) {
while(html) {
let textEnd = html.indexOf('<');
if (textEnd === 0) {
if (html.match(endTag)) {
//...process end tag
// endTag: 前面定义的正则
// 匹配到了结束标签
continue;
}
if (html.match(startTagOpen)) {
//...process start tag
// startTagOpen: 前面定义的正则
// 匹配到了开始标签
continue;
}
} else {
continue;
}
}
}
这里我们用到了前面定义的两个正则:endTag
和startTagOpen
。这里,我们把源码中的parseHTML
方法做了简化,方便理解。总的来看,就是利用了while
来循环解析template,用正则匹配结束和开始标签以及文本,分别来做不同的处理,直到整个template解析完成。
parseStartTag
到这里,我们大概了解了parseHTML
的原理。但是前面我们也说了,parseHTML
内部没有做任何处理,只是做了判断,直接continue了。
所以,这里的parseStartTag
的作用就是用来解析开始标签的。比如<div :class="c" class="demo" v-if="isShow">
。
该方法大概存在与Vue源码中的8866行, 此处我们也直接拿过来用。
function parseStartTag() {
const start = html.match(startTagOpen); // 匹配开始位置
if (start) {
// 成功匹配到了
const match = {
tagName: start[1], // 得到标签名
attrs: [], // 用来存放标签内属性
start: index // index: 全局变量,当前正则遍历的template字符位置
};
advance(start[0].length);
let end, attr; // 变量初始化
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))){
// 如果结束位置不等于标签的结束位置
// 并且属性也不匹配
// 说明该部分还没遍历完
advance(attr[0].length);
match.attrs.push({
name: attr[1],
value: attr[3]
});
}
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match;
}
}
}
首先,我们利用了startTagOpen
正则得到了标签的头部,拿到了tagName(标签名 start[1]),定义了attrs
空数组,用来存放标签内属性。
const start = html.match(startTagOpen);
const match = {
tagName: start[1], // 得到标签名
attrs: [], // 用来存放标签内属性
start: index // index: 全局变量,当前正则遍历的template字符位置
};
接下来,我们使用了while,用startTagClose
和attribute
两个正则来匹配解析标签的结束和标签内的属性,一直循环到startTagClose
,解析完内部所有属性为止。
stack
我们现在思考这样一个问题:标签是存在嵌套关系的,而我们解析是根据字符逐个解析的,这样就会导致嵌套关系的混乱,无法保存刚刚解析好的标签头。
为了解决这一问题,我们维护一个stack栈来保存已经解析好的标签头。
网友评论