概述
运行时版本和完整版本区别只是在于完整版本多了编译模板这个阶段,运行时版本会跳过模板编译阶段,因为,模板已经预先通过vue-loader
编译好了。
模板编译阶段
HTML 解析器作为主线,先用 html 解析器进行解析整个模板,如果在解析过程中碰见文本内容,就调用文本解析器来解析文本,如果碰到文本中包含过滤器,就调用过滤器解析器来解析
html 解析器主要是调用了 ParseHtml 函数,分别传入了 2 个参数,一个是待转换的模板字符串,一个是转换时所需要的选项。第二个参数在定义一些参数的同时,还定义了 4 个钩子函数,这 4 个钩子函数就是负责吧提取出来的内容生成对应的 ast。分别是 start,解析到标签的开始位置是触发。end,解析到标签的结束位置时触发。chars,解析到文本的时候触发。comment,解析到标签的注释是触发
通常模板内容会包含,文本,html 注释。条件注释,doctype,开始标签和结束标签。这几种内容都有其各自的特点,也就是根据这些不同内让那个所具有的不同特点编写不同的正则表达式,讲这些内容从模板字符串中解析出来
源码:
// 解析器主函数
// html解析器是主线,先用html解析器(parseHTML)解析整个模板。在解析过程中如果碰见文本内容,就调用文本解析器(parseText),如果碰见文本中包含过滤器,就调用过滤器解析器(parseFilters)来解析
parseHTML(template, {
// ...
// 解析到开始标签时,调用该函数
start(tag, attrs, unary, start, end) {
// ...
},
// 解析到结束标签时。调用该函数
end(tag, start, end) {
// ...
},
// 当解析到文本时,调用该函数
chars(text: string, start: number, end: number) {
// ...
},
// 解析到注释的时候,调用该函数
comment(text: string, start, end) {
// ...
},
});
export function parseHTML(html, options) {
// 维护ast层级的栈
const stack = [];
const expectHTML = options.expectHTML;
const isUnaryTag = options.isUnaryTag || no;
// 用来检测一个标签是否可以省略闭合标签的非自闭合标签
const canBeLeftOpenTag = options.canBeLeftOpenTag || no;
// 解析游标,标识从哪里开始解析
let index = 0;
// last存储剩余还未解析的模板字符串
// lastTag存储位于stack栈顶的元素
let last, lastTag;
// 循环html
while (html) {
last = html;
// 确保即将parse的内容不是在纯文本标签里,(script,style,textarea)
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf("<");
if (textEnd === 0) {
/**
* 如果html字符串以<开头,有以下几种可能
* 开始标签:<div>
* 结束标签:</div>
* 注释:<!-- 我是注释 -->
* 条件注释:<!-- [if !IE] --> <!-- [endif] -->
* DOCTYPE:<!DOCTYPE html>
*/
// 判断是否为注释
if (comment.test(html)) {
const commentEnd = html.indexOf("-->");
if (commentEnd >= 0) {
// ...
}
}
// 判断是否为条件注释
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf("]>");
if (conditionalEnd >= 0) {
// ...
}
}
// 判断是否为DOCTYPE
const doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue;
}
// 判断是否为结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
const curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue;
}
// 判断是否为开始标签
const startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue;
}
}
// 都不属于以上几种类型的,就是文本类型
let text, rest, next;
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf("<", 1);
if (next < 0) break;
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}
// 如果html字符串不是<开头的,表示为纯文本
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
// 提取文本
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
} else {
// 父元素为script、style、textarea时,其内部的内容全部当做纯文本处理
let endTagLength = 0;
const stackedTag = lastTag.toLowerCase();
const reStackedTag =
reCache[stackedTag] ||
(reCache[stackedTag] = new RegExp(
"([\\s\\S]*?)(</" + stackedTag + "[^>]*>)",
"i"
));
const rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (!isPlainTextElement(stackedTag) && stackedTag !== "noscript") {
text = text
.replace(/<!\--([\s\S]*?)-->/g, "$1") // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1");
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return "";
});
index += html.length - rest.length;
html = rest;
parseEndTag(stackedTag, index - endTagLength, index);
}
// 将整个字符串作为文本对待
if (html === last) {
options.chars && options.chars(html);
if (
process.env.NODE_ENV !== "production" &&
!stack.length &&
options.warn
) {
options.warn(`Mal-formatted tag at end of template: "${html}"`, {
start: index + html.length,
});
}
break;
}
}
// Clean up any remaining tags
parseEndTag();
function advance(n) {
index += n;
html = html.substring(n);
}
// parse开始标签
function parseStartTag() {
// ...
}
// 处理parseStartTag 结果
function handleStartTag(match) {
// ...
}
// parse 结束标签
function parseEndTag(tagName, start, end) {
// ...
}
}
// let text = "我叫{{name}},我今年{{age}}岁了"
// let res = parseText(text)
// res = {
// expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
// tokens:[
// "我叫",
// {'@binding': name },
// ",我今年"
// {'@binding': age },
// "岁了"
// ]
// }
export function parseText(
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
if (!tagRE.test(text)) {
return;
}
const tokens = [];
const rawTokens = [];
let lastIndex = (tagRE.lastIndex = 0);
let match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) {
// 先把{前面的文本放入tokens中
rawTokens.push((tokenValue = text.slice(lastIndex, index)));
tokens.push(JSON.stringify(tokenValue));
}
// 取出{{}}中间的变量
const exp = parseFilters(match[1].trim());
// 把变量改成_s(exp)的形式
tokens.push(`_s(${exp})`);
rawTokens.push({ "@binding": exp });
// 跳过}},下一轮从}}后面开始
lastIndex = index + match[0].length;
}
// 当剩下的text不能再被正则匹配上的时候,表示所有变量已经处理完毕
// 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本
if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join("+"),
tokens: rawTokens,
};
}
优化阶段
优化阶段主要做的就是两件事:意思在 ast 中找出所有静态节点并打上标记。二是在 ast 中找出所有静态根节点并打上标记
标记静态节点:
从根节点开始,先标记根节点是否为静态节点,如果根节点有子元素,就递归子元素,直到标记完所有节点,如果子节点不是一个静态节点,那么需要把子节点的父节点也标记为非静态节点。在模板编译阶段,会根据节点的类型给节点添加不同的 type 属性,分别标记为元素节点,包含变量的动态文本节点,不包含变量的纯文本节点。如果是包含变量的动态文本节点,那么肯定不是静态节点。如果是不包含变量的纯文本节点,那么肯定就是静态节点。如果是元素节点,要满足一下要求才能是静态节点,如果节点使用了v-pre
指令,那么断定是静态节点,如果没有使用到v-pre
指令,那要满足一下条件才能是静态节点,一是不能使用动态绑定语法,及标签上不能有v-
,@
,:
开头的属性,二是不能使用v-if
,v-for
等指令,三是不能为内置组件,即标签名不能为slot
和component
,四是标签名必须是平台保留标签,既不能是组件,五是当前节点的父节点不能是带有v-for
的template
标签。
标记静态根节点:
一个节点要成为静态根节点,就必须满足一下要求:一是节点本身必须是静态节点,二是必须拥有子节点,三是子节点不能只是只有一个文本节点
// 优化阶段:
// 在AST中找出所有静态节点并打上标记
// 在AST中找出所有静态根节点并打上标记;
export function optimize(root: ?ASTElement, options: CompilerOptions) {
if (!root) return;
isStaticKey = genStaticKeysCached(options.staticKeys || "");
isPlatformReservedTag = options.isReservedTag || no;
// 标记静态节点
markStatic(root);
// 标记静态根节点
markStaticRoots(root, false);
}
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// 包含变量的动态文本节点
return false;
}
if (node.type === 3) {
// 不包含变量的文本节点
return true;
}
// node.type==1,为元素节点,需要进一步判断
// 1、如果节点使用了v-pre就是静态节点
// 2、如果没使用v-pre,它要成为静态节点必须满足:
// 2.1、不能使用动态绑定语法,v-,@,:,开头的属性
// 2.2、不能使用v-if,v-else,v-for指令
// 2.3、不能是内置组件,即slot和component
// 2.4、不能是组件
// 2.5、当前节点的父节点不能带有v-for的template标签
// 2.6、节点所有属性的key必须是静态的
return !!(
node.pre ||
(!node.hasBindings && // no dynamic bindings
!node.if &&
!node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey))
);
}
function markStatic(node: ASTNode) {
node.static = isStatic(node);
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== "slot" &&
node.attrsMap["inline-template"] == null
) {
return;
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i];
markStatic(child);
if (!child.static) {
// 如果一开始父节点被标记为静态节点,但是子节点不是静态的,需要把父节点标记为非静态节点
node.static = false;
}
}
// 如果使用了v-if,v-else等指令,没被渲染的不在node.children中,所以还要遍历node.ifConditions
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block;
markStatic(block);
if (!block.static) {
node.static = false;
}
}
}
}
}
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// 成为静态根节点的要求:
// 1、节点本身必须是子节点
// 2、必须拥有子节点
// 3、子节点不能只是只有一个文本节点
// 否则优化成本大于优化后带来的利益
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true;
return;
} else {
node.staticRoot = false;
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor);
}
}
}
}
代码生成阶段
代码生成阶段就是要生成 render 函数字符串。
v-if 和 v-for,在代码生成阶段,不同指令和内容处理的优先级是不一样的。优先级如下:静态节点,v-once 指令,v-for 指令,v-if 指令,template 包裹的子节点,插槽
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}
if (el.staticRoot && !el.staticProcessed) {
// 静态节点
return genStatic(el, state);
} else if (el.once && !el.onceProcessed) {
// v-once
return genOnce(el, state);
} else if (el.for && !el.forProcessed) {
// v-for
return genFor(el, state);
} else if (el.if && !el.ifProcessed) {
// v-if
return genIf(el, state);
} else if (el.tag === "template" && !el.slotTarget && !state.pre) {
// template
return genChildren(el, state) || "void 0";
} else if (el.tag === "slot") {
// 插槽
return genSlot(el, state);
} else {
// 元素或者组件
// component or element
// ...
}
}