template 模板是怎样通过 Compile 编译的
Compile
compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
这部分内容不算 Vue.js 的响应式核心,只是用来编译的,笔者认为在精力有限的情况下不需要追究其全部的实现细节,能够把握如何解析的大致流程即可。
由于解析过程比较复杂,直接上代码可能会导致不了解这部分内容的同学一头雾水。
所以笔者准备提供一个 template 的示例,通过这个示例的变化来看解析的过程。
但是解析的过程及结果都是将最重要的部分抽离出来展示,希望能让读者更好地了解其核心部分的实现。
{{item}}
var html = '{{item}}';
接下来的过程都会依赖这个示例来进行。
parse
首先是 parse,parse 会用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST(在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。)。
这个过程比较复杂,会涉及到比较多的正则进行字符串解析,我们来看一下得到的 AST 的样子。
{
/* 标签属性的map,记录了标签上属性 */
'attrsMap': {
':class': 'c',
'class': 'demo',
'v-if': 'isShow'
},
/* 解析得到的:class */
'classBinding': 'c',
/* 标签属性v-if */
'if': 'isShow',
/* v-if的条件 */
'ifConditions': [
{
'exp': 'isShow'
}
],
/* 标签属性class */
'staticClass': 'demo',
/* 标签的tag */
'tag': 'div',
/* 子标签数组 */
'children': [
{
'attrsMap': {
'v-for': "item in sz"
},
/* for循环的参数 */
'alias': "item",
/* for循环的对象 */
'for': 'sz',
/* for循环是否已经被处理的标记位 */
'forProcessed': true,
'tag': 'span',
'children': [
{
/* 表达式,_s是一个转字符串的函数 */
'expression': '_s(item)',
'text': '{{item}}'
}
]
}
]
}
最终得到的 AST 通过一些特定的属性,能够比较清晰地描述出标签的属性以及依赖关系。
接下来我们用代码来讲解一下如何使用正则来把 template 编译成我们需要的 AST 的。
正则
首先我们定义一下接下来我们会用到的正则。
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('^/
const endTag = new RegExp('^]*>')
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
advance
因为我们解析 template 采用循环进行字符串匹配的方式,所以每匹配解析完一段我们需要将已经匹配掉的去掉,头部的指针指向接下来需要匹配的部分。
function advance (n) {
index += n
html = html.substring(n)
}
举个例子,当我们把第一个 div 的头标签全部匹配完毕以后,我们需要将这部分除去,也就是向右移动 43 个字符。

调用 advance 函数
advance(43);
得到结果

parseHTML
首先我们需要定义个 parseHTML 函数,在里面我们循环解析 template 字符串。
function parseHTML () {
while(html) {
let textEnd = html.indexOf('”`的情况,这时候就要找到 stack 中的第二个位置才能找到同名标签。
```js
function parseEndTag (tagName) {
let pos;
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === tagName.toLowerCase()) {
break;
}
}
if (pos >= 0) {
stack.length = pos;
currentParent = stack[pos];
}
}
parseText
最后是解析文本,这个比较简单,只需要将文本取出,然后有两种情况,一种是普通的文本,直接构建一个节点 push 进当前 currentParent 的 children 中即可。
还有一种情况是文本是如“{ {item} }”这样的 Vue.js 的表达式,这时候我们需要用 parseText 来将表达式转化成代码。
text = html.substring(0, textEnd)
advance(textEnd)
let expression;
if (expression = parseText(text)) {
currentParent.children.push({
type: 2,
text,
expression
});
} else {
currentParent.children.push({
type: 3,
text,
});
}
continue;
我们会用到一个 parseText 函数。
function parseText (text) {
if (!defaultTagRE.test(text)) return;
const tokens = [];
let lastIndex = defaultTagRE.lastIndex = 0
let match, index
while ((match = defaultTagRE.exec(text))) {
index = match.index
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
const exp = match[1].trim()
tokens.push(`_s(${exp})`)
lastIndex = index + match[0].length
}
if (lastIndex hello,{{name}}.
最终得到 tokens。
tokens = ['hello,', _s(name), '.'];
最终通过 join 返回表达式。
'hello' + _s(name) + '.';
processIf与processFor
最后介绍一下如何处理“v-if”以及“v-for”这样的 Vue.js 的表达式的,这里我们只简单介绍两个示例中用到的表达式解析。
我们只需要在解析头标签的内容中加入这两个表达式的解析函数即可,在这时“v-for”之类指令已经在属性解析时存入了 attrsMap 中了。
if (html.match(startTagOpen)) {
const startTagMatch = parseStartTag();
const element = {
type: 1,
tag: startTagMatch.tagName,
attrsList: startTagMatch.attrs,
attrsMap: makeAttrsMap(startTagMatch.attrs),
parent: currentParent,
children: []
}
processIf(element);
processFor(element);
if(!root){
root = element
}
if(currentParent){
currentParent.children.push(element);
}
stack.push(element);
currentParent = element;
continue;
}
首先我们需要定义一个 getAndRemoveAttr 函数,用来从 el 的 attrsMap 属性或是 attrsList 属性中取出 name 对应值。
function getAndRemoveAttr (el, name) {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i {
return new VNode('span', {}, [
createTextVNode(item);
]);
})
/* end */
)) : createEmptyVNode();
}
那我们如何来实现一个 generate 呢?
genIf
首先实现一个处理 if 条件的 genIf 函数。
function genIf (el) {
el.ifProcessed = true;
if (!el.ifConditions.length) {
return '_e()';
}
return `(${el.ifConditions[0].exp})?${genElement(el.ifConditions[0].block)}: _e()`
}
genFor
然后是处理 for 循环的函数。
function genFor (el) {
el.forProcessed = true;
const exp = el.for;
const alias = el.alias;
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '';
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '';
return `_l((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${genElement(el)}` +
'})';
}
genText
处理文本节点的函数。
function genText (el) {
return `_v(${el.expression})`;
}
genElement
接下来实现一下 genElement,这是一个处理节点的函数,因为它依赖 genChildren 以及g enNode ,所以这三个函数放在一起讲。
genElement会根据当前节点是否有 if 或者 for 标记然后判断是否要用 genIf 或者 genFor 处理,否则通过 genChildren 处理子节点,同时得到 staticClass、class 等属性。
genChildren 比较简单,遍历所有子节点,通过 genNode 处理后用“,”隔开拼接成字符串。
genNode 则是根据 type 来判断该节点是用文本节点 genText 还是标签节点 genElement 来处理。
function genNode (el) {
if (el.type === 1) {
return genElement(el);
} else {
return genText(el);
}
}
function genChildren (el) {
const children = el.children;
if (children && children.length > 0) {
return `${children.map(genNode).join(',')}`;
}
}
function genElement (el) {
if (el.if && !el.ifProcessed) {
return genIf(el);
} else if (el.for && !el.forProcessed) {
return genFor(el);
} else {
const children = genChildren(el);
let code;
code = `_c('${el.tag},'{
staticClass: ${el.attrsMap && el.attrsMap[':class']},
class: ${el.attrsMap && el.attrsMap['class']},
}${
children ? `,${children}` : ''
})`
return code;
}
}
generate
最后我们使用上面的函数来实现 generate,其实很简单,我们只需要将整个 AST 传入后判断是否为空,为空则返回一个 div 标签,否则通过 generate 来处理。
function generate (rootAst) {
const code = rootAst ? genElement(rootAst) : '_c("div")'
return {
render: `with(this){return ${code}}`,
}
}
经历过这些过程以后,我们已经把 template 顺利转成了 render function 了,接下来我们将介绍 patch 的过程,来看一下具体 VNode 节点如何进行差异的比对。
注:本节代码参考 《template 模板是怎样通过 Compile 编译的》。