vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js)
模板编译核心由三部分组成:
- 解析,将类 html 模版转换为 AST 对象(Abstract Syntax Tree,抽象语法树)
- 优化,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点,以及标记出静态根节点
- 代码生成器,将 AST 对象生成渲染函数字符串
AST对象描述
AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。Vue 源码中借鉴 jQuery 作者 John Resig 的 HTML Parser 对模板进行解析,得到的就是 AST 代码。
AST只是用JS中的对象来描述一个节点,一个对象代表一个节点,对象中的属性用来保存节点所需的各种数据。
以下是Vue中对AST数据的定义(代码在 https://github.com/vuejs/vue/blob/dev/flow/compiler.js中)
declare type ASTNode = ASTElement | ASTText | ASTExpression;
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<ASTAttr>;
attrsMap: { [key: string]: any };
rawAttrsMap: { [key: string]: ASTAttr };
parent: ASTElement | void;
children: Array<ASTNode>;
start?: number;
end?: number;
processed?: true;
static?: boolean;
staticRoot?: boolean;
staticInFor?: boolean;
staticProcessed?: boolean;
hasBindings?: boolean;
text?: string;
attrs?: Array<ASTAttr>;
dynamicAttrs?: Array<ASTAttr>;
props?: Array<ASTAttr>;
plain?: boolean;
pre?: true;
ns?: string;
component?: string;
inlineTemplate?: true;
transitionMode?: string | null;
slotName?: ?string;
slotTarget?: ?string;
slotTargetDynamic?: boolean;
slotScope?: ?string;
scopedSlots?: { [name: string]: ASTElement };
ref?: string;
refInFor?: boolean;
if?: string;
ifProcessed?: boolean;
elseif?: string;
else?: true;
ifConditions?: ASTIfConditions;
for?: string;
forProcessed?: boolean;
key?: string;
alias?: string;
iterator1?: string;
iterator2?: string;
staticClass?: string;
classBinding?: string;
staticStyle?: string;
styleBinding?: string;
events?: ASTElementHandlers;
nativeEvents?: ASTElementHandlers;
transition?: string | true;
transitionOnAppear?: boolean;
model?: {
value: string;
callback: string;
expression: string;
};
directives?: Array<ASTDirective>;
forbidden?: true;
once?: true;
onceProcessed?: boolean;
wrapData?: (code: string) => string;
wrapListeners?: (code: string) => string;
// 2.4 ssr optimization
ssrOptimizability?: number;
// weex specific
appendAsTree?: boolean;
};
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
tokens: Array<string | Object>;
static?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
start?: number;
end?: number;
};
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
// 2.4 ssr optimization
ssrOptimizability?: number;
start?: number;
end?: number;
};
可以看到 ASTNode
有三种形式:ASTElement
,ASTExpression
,ASTText
。
举例看下:
<div id="app">
<h1>{{message}}</h1>
</div>
该模板转换成AST后的结构如下:
{
tag: "div",
type: 1,
plain: false,
start: 0,
end: 44,
parent: undefined,
attrs: [{
dynamic: undefined,
end: 13,
name: "id",
start: 5,
value: "app",
}],
attrsList: [{
end: 13,
name: "id",
start: 5,
value: "app"
}],
attrsMap: {
id: "app"
},
rawAttrsMap: {
id: {
name: "id",
value: "app",
start: 5,
end: 13
}
},
children: [{
tag: "h1",
type: 1,
type: 1,
plain: true,
start: 17,
end: 37,
parent: {...},
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
children: [{
end: 32,
expression: "_s(message)",
start: 21,
text: "{{message}}",
type: 2,
tokens: [{
"@binding": "message"
}]
}]
}]
}
parse函数伪代码:
function parse (template, options) {
...
var stack = [];
var root; // 最终返回出去的AST树根节点
var currentParent; // 当前父节点
...
parseHTML(template, {
start: function start (tag, attrs, unary, start$1, end) {
// 每当解析到标签的开始位置时,触发该函数
...
},
end: function end (tag, start, end$1) {
// 每当解析到标签的结束位置时,触发该函数
...
},
chars: function chars (text, start, end) {
// 每当解析到文本时,触发该函数
...
},
comment: function comment (text, start, end) {
// 每当解析到注释时,触发该函数
...
}
});
return root
}
以下面模板为例
<div id="app">
<h1>{{message}}</h1>
</div>
解析上面的模版,从前向后解析,依次触发 start、start、chars、end、end 钩子函数
解析到 | <div> | 触发 start |
解析到 | <h1> | 触发 start |
解析到 | {{message}} | 触发chars |
解析到 | </h1> | 触发end |
解析到 | </div> | 触发end |
各个钩子函数如何构建 AST 节点?
start 钩子函数
function createASTElement(tag, attrs, parent) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: [],
};
}
parseHTML(template, {
start: function start(tag, attrs, unary, start$1, end) {
// ...
var element = createASTElement(tag, attrs, currentParent);
// ...
if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
},
});
end钩子函数
function closeElement(element) {
// ...
currentParent.children.push(element);
element.parent = currentParent;
}
parseHTML(template, {
end: function end(tag, start, end$1) {
var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
},
});
chars钩子函数
parseHTML(template, {
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,
};
}
// ...
},
});
comment钩子函数
parseHTML(template, {
comment: function comment(text, start, end) {
// ...
var child = {
type: 3,
text: text,
isComment: true,
};
// ...
},
});
上面构建出来的节点是独立的,我们需要一套逻辑把这些节点连起来,构成一个真正的 AST。
如何构建 AST 层级关系
解析 html 的时候,我们需要维护一个栈 (stack),用 stack 来记录层级关系,也可以理解为 DOM 的深度。
每当遇到开始标签,触发 start 钩子函数;每当遇到结束标签,触发 end 钩子函数。
基于以上情况,我们在触发 start 钩子函数时,将当前构建的节点推入 stack 中;触发 end 钩子函数时,从 stack 中弹出一个节点。
这样就可以保证每当触发 start 钩子函数时,stack 的最后一个节点就是当前正在构建的节点的父节点。
以下面模板为例
<div id="app">
<h1>{{message}}</h1>
</div>
解析时具体细节
解析时的模板 | 解析到 | 解析后的stack | 解析后的AST | 解析后 |
<div id="app">\n <h1>{{message}}</h1>\n</div> | <div id="app"> | ["div"] | {tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13} ]} | 模版中 <div id="app"> 被截取掉 |
\n <h1>{{message}}</h1>\n</div> | 空格 | ["div"] | {tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13} ]} | 空格被截取掉 |
<h1>{{message}}</h1>\n</div> | <h1> | ["div", "h1"] | {tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13} tag: 'h1', ... }]} | <h1>标签被截取掉 |
{{message}}</h1>\n</div> | {{message}} | ["div", "h1"] | {tag: "div", attrs: [ {name: "id", value: "app", start: 5, end: 13} tag: 'h1', ..., children: [{ }]} | {{message}}被截取掉 |
</h1>\n</div> | </h1> | ["div"] | 同上 | </h1>被截取掉 |
\n</div> | 空格 | ["div"] | 同上 | 空格被截取掉 |
</div> | </div> | [] | 同上 | </div>被截取掉 |
- | html 模版为空,解析完成 | [] | 同上 | - |
在看parseHTML方法之前先熟悉几个正则表达式:
获取属性的正则
// 获取属性的正则
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
解读如下:
- ([^\s"'<>\/=]+) 可以匹配属性的key部分,如匹配 id="app"中的id
- (?:\s*(=)\s*) 是一个非捕获性分组,用来匹配属性中的 = 号 当然匹配前后可能存在的空格号
- (?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)) 是一个非捕获性分组,用来匹配属性的value部分, 属性分为三种情况,有 id="app", id='app' , id=app 这三种情况
举例说明:
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var str = 'id="app"', str1 = "id='app'", str2 = "id=app";
console.log(str.match(attribute));
// ["id=\"app\"", "id", "=", "app", undefined, undefined, index: 0, input: "id=\"app\"", groups: undefined]
console.log(str1.match(attribute));
// ["id='app'", "id", "=", undefined, "app", undefined, index: 0, input: "id='app'", groups: undefined]
console.log(str2.match(attribute));
// ["id=app", "id", "=", undefined, undefined, "app", index: 0, input: "id=app", groups: undefined]
获取动态属性的正则
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
获取动态属性的正则表达式只比获取普通属性的正则表达式多了 这一部分
匹配包含v-、@、:、# 等 Vue 专有的动态属性
其他跟上面一样
举例说明:
var attribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var str = 'v-bind:[attributename]="url"', str1 = "v-bind:[attributename]='url'", str2 = "v-bind:[attributename]=url";
console.log(str.match(attribute));
// ["v-bind:[attributename]=\"url\"", "v-bind:[attributename]", "=", "url", undefined, undefined, index: 0, input: "v-bind:[attributename]=\"url\"", groups: undefined]
console.log(str1.match(attribute));
// ["v-bind:[attributename]='url'", "v-bind:[attributename]", "=", undefined, "url", undefined, index: 0, input: "v-bind:[attributename]='url'", groups: undefined]
console.log(str2.match(attribute));
// ["v-bind:[attributename]=url", "v-bind:[attributename]", "=", undefined, undefined, "url", index: 0, input: "v-bind:[attributename]=url", groups: undefined]
处理开始/结束标签的正则
// 用于解析html标记、组件名称和属性的unicode合法字符
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
// 合法字符的集合
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
// 表示匹配 xxx:xxx 或 xxx 模式的字符
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
// 匹配标签开始部分,即:<xxx:xxx 或 <xxx 的模式, 如匹配<div或<xml:xxx或<my-component
var startTagOpen = new RegExp(("^<" + qnameCapture));
// 对开始标签的结束进行匹配,可判断是否是自闭合标签
var startTagClose = /^\s*(\/?)>/;
// 匹配标签结束部分, 如</div>或</xml:xxx>或</my-component>
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
startTagOpen正则可视化:
处理页面开始的 doctype 声明标签正则
var doctype = /^<!DOCTYPE [^>]+>/i;
处理注释部分正则
var comment = /^<!\--/;
处理条件注释部分正则 (出现在要对 ie 浏览器进行版本渲染的情况)
// 匹配条件注释 如<!--[if !IE]>-->我是注释<!--<![endif]-->
var conditionalComment = /^<!\[/;
Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。
下面是parseHTML函数的内部实现:
function parseHTML(html, options) {
var stack = [];
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no;
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0;
var last, lastTag;
while (html) {
// 保留 html 副本
last = html;
// 如果没有lastTag,并确保我们不是在一个纯文本内容元素中:script、style、textarea
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf("<");
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
// ...
}
// conditionalComment
if (conditionalComment.test(html)) {
// ...
}
// Doctype:
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
// ...
}
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
// ...
}
// Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
// ...
}
}
var text = void 0,
rest = void 0,
next = void 0;
if (textEnd >= 0) {
// ...
}
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
} else {
// ...
parseEndTag(stackedTag, index - endTagLength, index);
}
if (html === last) {
options.chars && options.chars(html);
// ...
break;
}
}
// ...
}
可以看出上面代码大概意思:
1. 循环html模板
2. 正则判断html字符串是开始标签、结束标签或文本标签,并分别进行处理
还是以下面模板来解析
<div id="app">
<h1>{{message}}</h1>
</div>
解析开始标签
/**
* 匹配到元素的名字和属性,保存到match对象中并返回
* @return {tagName: string, attrs: Array, start: number}
*/
function parseStartTag() {
// 判断html中是否存在开始标签
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1], // 标签名
attrs: [], // 属性集合
start: index, // 标签的开始位置
};
// 截取掉匹配到的字符串
advance(start[0].length);
var end, attr;
// 判断不是开始标签结尾并且存在属性
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(dynamicArgAttribute) || html.match(attribute))
) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
// 如果解析到结尾,要判断该标签是否是自闭合标签
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match;
}
}
}
解析完开始标签后,我们得到了一个起始标签match
的数据结构
/**
* 处理解析后的属性,重新分割并保存到attrs数组中
* @param match
*/
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
if (expectHTML) {
if (lastTag === "p" && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);
}
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
}
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || ""; // 属性值
var shouldDecodeNewlines =
tagName === "a" && args[1] === "href"
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines;
// 改变attr的格式为 [{name: 'id', value: 'app'}]
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines),
};
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
// 如果不是自闭合标签
// stack为了存放标签名 为了和结束标签进行匹配的作用
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end,
});
lastTag = tagName;
}
if (options.start) {
// 触发start钩子函数
options.start(tagName, attrs, unary, match.start, match.end);
}
}
start钩子函数
/**
* 这个和end相对应,主要处理开始标签和标签的属性(内置和普通属性),
* @param tag 标签名
* @param attrs 元素属性
* @param unary 该元素是否自闭合标签, 如img
*/
start(tag, attrs, unary, start$1, end) {
// check namespace.
// inherit parent ns if there is one
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
}
// 创建基础的 ASTElement
var element = createASTElement(tag, attrs, currentParent);
if (ns) {
element.ns = ns;
}
// ...
// apply pre-transforms
for (var i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}
if (!inVPre) {
// 判断有没有 v-pre 指令的元素。如果有的话 element.pre = true
// 官网有介绍:<span v-pre>{{ this will not be compiled }}</span>
// 跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
// 处理原始属性
processRawAttrs(element);
} else if (!element.processed) {
// structural directives
// v-for v-if v-once
processFor(element);
processIf(element);
processOnce(element);
}
if (!root) {
root = element;
{
// 检查根节点约束
checkRootConstraints(root);
}
}
if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
}
function closeElement(element) {
trimEndingWhitespace(element);
if (!inVPre && !element.processed) {
element = processElement(element, options);
}
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
{
checkRootConstraints(element);
}
addIfCondition(root, {
exp: element.elseif,
block: element
});
} else {
warnOnce(
"Component template should contain exactly one root element. " +
"If you are using v-if on multiple elements, " +
"use v-else-if to chain them instead.",
{ start: element.start }
);
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
var name = element.slotTarget || '"default"'
; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
// 将元素插入 children 数组中
currentParent.children.push(element);
element.parent = currentParent;
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(function (c) { return !(c).slotScope; });
// remove trailing whitespace node again
trimEndingWhitespace(element);
// check pre state
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
// apply post-transforms
for (var i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
}
其实start
方法就是处理 element
元素的过程。确定命名空间;创建AST元素 element;执行预处理;定义root;处理各类 v- 标签的逻辑;最后更新 root、currentParent、stack 的结果。
解析结束标签
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
/**
* 解析关闭标签,
* 查找我们之前保存到stack栈中的元素,
* 如果找到了,也就代表这个标签的开始和结束都已经找到了,此时stack中保存的也就需要删除(pop)了
* 并且缓存最近的标签lastTag
* @param tagName
* @param start
* @param end
*/
function parseEndTag(tagName, start, end) {
var pos, lowerCasedTagName;
if (start == null) { start = index; }
if (end == null) { end = index; }
// Find the closest opened tag of the same type
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0;
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (i > pos || !tagName &&
options.warn
) {
options.warn(
("tag <" + (stack[i].tag) + "> has no matching end tag."),
{ start: stack[i].start, end: stack[i].end }
);
}
if (options.end) {
options.end(stack[i].tag, start, end);
}
}
// Remove the open elements from the stack
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
end钩子函数
end(tag, start, end$1) {
var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
}
解析注释
// Comment:
if (comment.test(html)) {
var commentEnd = html.indexOf('-->');
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
advance(commentEnd + 3);
continue
}
}
// comment钩子函数
comment(text, start, end) {
// adding anyting as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
var child = {
type: 3,
text: text,
isComment: true
};
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
currentParent.children.push(child);
}
}
解析条件注释
// 条件注释会被直接截取掉
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>');
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}
解析DOCTYPE
// Doctype:
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}
解析文本
解析文本分为两种情况:
1. 父元素为正常元素的处理逻辑
2. 父元素为script、style、textarea的处理逻辑
先来看第一种:
处理父元素为正常元素的文本
var textEnd = html.indexOf('<');
// ...
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {
rest = html.slice(textEnd);
// 1 < 2</div>, 包含了 < 符号的处理
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);
}
// 没有,则整个都是文本
if (textEnd < 0) {
text = html;
}
if (text) {
advance(text.length);
}
// 调用 chars 钩子
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
char钩子函数
chars(text, start, end) {
// 如果是文本,没有父节点,直接返回
if (!currentParent) {
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
var children = currentParent.children;
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = '';
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' ';
} else {
text = ' ';
}
} else {
text = preserveWhitespace ? ' ' : '';
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE$1, ' ');
}
var res;
var child;
// 解析文本,处理{{xxx}} 这种形式的文本
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
};
}
if (child) {
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
}
parseText方法
function parseText(text, delimiters) {
var tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
if (!tagRE.test(text)) {
return
}
var tokens = [];
var rawTokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, tokenValue;
// exec中不管是不是全局的匹配,只要没有子表达式,
// 其返回的都只有一个元素,如果是全局匹配,可以利用lastIndex进行下一个匹配,
// 匹配成功后lastIndex的值将会变为上次匹配的字符的最后一个位置的索引。
// 在设置g属性后,虽然匹配结果不受g的影响,
// 返回结果仍然是一个数组(第一个值是第一个匹配到的字符串,以后的为分组匹配内容),
// 但是会改变index和 lastIndex等的值,将该对象的匹配的开始位置设置到紧接这匹配子串的字符位置,
// 当第二次调用exec时,将从lastIndex所指示的字符位置 开始检索。
while ((match = tagRE.exec(text))) {
index = match.index;
// 当文本标签中既有{{}} 在其左边又有普通文本时,
// 如:<span>我是普通文本{{value}}</span>, 就会执行下面的方法,添加到tokens数组中。
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
// 把匹配到{{}}中的tag 添加到tokens数组中
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({ '@binding': exp });
lastIndex = index + match[0].length;
}
// 当文本标签中既有{{}} 在其右边又有普通文本时,
// 如:<span>{{value}} 我是普通文本</span>, 就会执行下面的方法,添加到tokens数组中。
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
处理纯文本内容元素
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
// 父元素为正常元素的处理逻辑
} else {
// 父元素为script、style、textarea的处理逻辑
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
// 正则表达式可以匹配结束标签前包括结束标签自身在内的所有文本
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var rest$1 = 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$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
}
如下面模板
<div id="app">
<script>console.log(1)</script>
</div>
当解析到script中的内容时,模板是下面的样子:
console.log(1)</script>
</div>
此时父元素为script,所以会进入到else中的逻辑进行处理。在其处理过程中,会触发钩子函数chars和end。
钩子函数chars的参数为script中的所有内容:
chars('console.log(1)')
总结
解析器的作用是通过模板得到AST(抽象语法树)。
生成AST的过程需要借助HTML解析器,当HTML解析器触发不同的钩子函数时,我们可以构建出不同的节点。
随后,我们可以通过栈来得到当前正在构建的节点的父节点,然后将构建出的节点添加到父节点的下面。
最终,当HTML解析器运行完毕后,我们就可以得到一个完整的带DOM层级关系的AST。
HTML解析器的内部原理是一小段一小段地截取模板字符串,每截取一小段字符串,就会根据截取出来的字符串类型触发不同的钩子函数,直到模板字符串截空停止运行。
文本分两种类型,不带变量的纯文本和带变量的文本,后者需要使用文本解析器进行二次加工。
参考资料: