原稿:语雀 · 【原理三】模板编译 · 原目录:Vue › 【原理三】模板编译
Vue2.x 实例化到渲染
Vue2.x 模板编译
第一部分大图中 compileToFunction() 调用 compile(),定义位置即下方的 baseCompile()

var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
// 1.解析
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 2.优化
optimize(ast, options);
}
// 3.代码生成
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});

说明 vue 模板编译过程分为3个阶段: 2. 解析:将模板字符串解析生成 AST,生成的 AST 元素节点总共有 3 种类型:普通元素、表达式、纯文本 4. 优化语法树:Vue 模板中并不是所有数据都是响应式的,有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 过程跳过比对,直接复用。优化阶段就是标记 AST Element 得静态根节点或静态节点 6. 生成代码:通过 generate 方法,将 ast 生成可执行的 render 函数
parse 过程
参考链接:
Vue 中对于模板的解析主要在 parse() 中进行,其中又会调用 parseHTML() ,并通过 options 传入四个钩子函数,在遇到不同标签时触发
parseHTML(template, {
start (tag, attrs, unary) { // 每当解析到标签的开始位置时,触发该函数 },
end () { // 每当解析到标签的结束位置时,触发该函数 },
chars (text) { // 每当解析到文本时,触发该函数 },
comment (text) { // 每当解析到注释时,触发该函数 }
})
循环解析
解析模板的过程就是循环的过程:用模板字符串来循环,每轮循环都从模板中截取一小段字符串,并触发相应的的钩子函数,重复直到模板被截成一个空字符串时结束循环,解析完毕
function parseHTML(html, options) {
while (html) {
// 截取模板字符串并触发钩子函数
}
}
假设有以上的 HTML 模板,它在被 HTML 解析器解析的过程如下:
`<div>
<p>{{name}}</p>
</div>`
1. 第一轮循环时,截取出一段字符串<div>,并且触发钩子函数start,截取后的结果为` <p>{{name}}</p></div>`=======================================================================================2. 第二轮循环时,截取出一段字符串` `并且触发钩子函数chars,截取后的结果为`<p>{{name}}</p></div>`=======================================================================================3. 第三轮循环时,截取出一段字符串<p>,并且触发钩子函数start,截取后的结果为`{{name}}</p></div>`=======================================================================================4. 第四轮循环时,截取出一段字符串{{name}},并且触发钩子函数chars,截取后的结果为:`</p></div>`=======================================================================================5. 第五轮循环时,截取出一段字符串</p>,并且触发钩子函数end,截取后的结果为:`</div>`=======================================================================================6. 第六轮循环时,截取出一段字符串:``并且触发钩子函数chars,截取后的结果为:`</div>`=======================================================================================
处理不同标签
在循环中需要判断具体的标签类型,做出不同的处理
export function parseHTML (html, options) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// 注释
if (comment.test(html)) {}
// 条件注释
if (conditionalComment.test(html)) {}
// DOCTYPE
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {}
// 结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {}
// 开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {}
}
let text, rest, next
if (textEnd >= 0) {}
if (textEnd < 0) {}
if (options.chars && text) {}
} else {
// 父元素为script、style、textarea的处理逻辑
}
}
}
- if (!lastTag || !isPlainTextElement(lastTag)):判断是否是纯文本(script、style、textarea)
- if (comment.test(html)):判断是否是注释
- if (conditionalComment.test(html)):判断是否是条件注释
- if (doctypeMatch):判断是否是 DOCTYPE
- if (endTagMatch):判断是否是结束标签,其中调用 parseEndTag(),触发 end 钩子



start 函数
start 钩子中 2. 初始化 AST 元素 4. 应用 preTransforms(在建立 ASTElement 基本信息后,进行节点的信息转换处理) 6. 处理动态指令
通过 start 钩子可以创建 ASTElement 结点
start: function start (tag, attrs, unary, start$1, end) {
var element = createASTElement(tag, attrs, currentParent);
// apply pre-transforms
for (var i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}
if (!inVPre) {
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
processRawAttrs(element);
} else if (!element.processed) {
// structural directives
processFor(element);
processIf(element);
processOnce(element);
}
}
chars 函数
chars: function chars (text, start, end) {
//...
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text: text
};
}
//...
}
chars 钩子会调用 parseText(),后者会进行文本解析:使用正则判断文本是否带有变量,也就是检查文本中是否包含 {{xxx}} 语法
- 如果是纯文本,则直接返回 undefined
- 如果是带变量的文本,先把变量左边的文本添加到数组中,然后把变量改成 _s(x) 形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。当最后一个变量的后面还有文本,将它添加到数组中
function parseText (text) {
const tagRE = /\{\{((?:.|\n)+?)\}\}/g
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
while ((match = tagRE.exec(text))) {
index = match.index
// 先把 {{ 前边的文本添加到tokens中
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// 把变量改成`_s(x)`这样的形式也添加到数组中
tokens.push(`_s(${match[1].trim()})`)
// 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
lastIndex = index + match[0].length
}
// 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
}
parseText() 的效果如下
parseText('你好{{name}}, 你今年已经{{age}}岁啦')
// '"你好"+_s(name)+", 你今年已经"+_s(age)+"岁啦"'
回到 char 函数中:
- 带变量的文本解析后会作为 AST 元素的 expression 属性,最后生成 ASTExpression 结点
- 纯文本会对应一个纯文本的对象,最后生成 ASTText 结点
小结 Parse 过程 HTMLParser 在解析模版的过程中,将节点信息通过 start,end 和 chars 三个方法传递给 Vue.js 的 ast 构建程序,边解析边同时构建模版的 ast,最终生成一个完整的 AST 树 模版的 AST 节点有 3 种类型 ASTElement,ASTText 和 ASTExpression,对应 type 的1,2,3
|