vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
parseHTML
hooks上一篇我们分析了parse
方法中的parseHTML
,在parseHTML
中会调用很多的hooks,这些hooks定义在:
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start(tag, attrs, unary, start, end) { ... },
end(tag, start, end) { ... },
chars(text: string, start: number, end: number) { ... },
comment(text: string, start, end) { ... },
}
)
return root;
总共有四个hook:start
,end
,chars
,comment
。从字面意思上可以看出分别会在标签开始、标签结束、文本节点、注释节点中调用。
我们一个一个来分析,先看start
。
start
start(tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns =
(currentParent && currentParent.ns) || platformGetTagNamespace(tag);
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === "svg") {
attrs = guardIESVGBug(attrs);
}
let element: ASTElement = createASTElement(tag, attrs, currentParent);
if (ns) {
element.ns = ns;
}
if (process.env.NODE_ENV !== "production") {
if (options.outputSourceRange) {
element.start = start;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr;
return cumulated;
}, {});
}
attrs.forEach((attr) => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length,
}
);
}
});
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
process.env.NODE_ENV !== "production" &&
warn(
"Templates should only be responsible for mapping the state to the " +
"UI. Avoid placing tags with side-effects in your templates, such as " +
`<${tag}>` +
", as they will not be parsed.",
{ start: element.start }
);
}
// apply pre-transforms
for (let 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);
}
if (!root) {
root = element;
if (process.env.NODE_ENV !== "production") {
checkRootConstraints(root);
}
}
if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
},
代码非常的多,我们一点一点来看,同时也会跳过一些分主线的逻辑。
let element: ASTElement = createASTElement(tag, attrs, currentParent);
if (ns) {
element.ns = ns;
}
创建了一个AST元素,同时也有命名空间的判断,继续。
if (process.env.NODE_ENV !== "production") {
if (options.outputSourceRange) {
element.start = start;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr;
return cumulated;
}, {});
}
attrs.forEach((attr) => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
{
start: attr.start + attr.name.indexOf(`[`),
end: attr.start + attr.name.length,
}
);
}
});
}
从报错可以看出这里的逻辑是对HTML属性的规范判断,继续。
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
process.env.NODE_ENV !== "production" &&
warn(
"Templates should only be responsible for mapping the state to the " +
"UI. Avoid placing tags with side-effects in your templates, such as " +
`<${tag}>` +
", as they will not be parsed.",
{ start: element.start }
);
}
这里是如果你是客户端渲染,防止你在模板里写<style>和<script>,继续。
if (!inVPre) {
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
v-pre
环境的判断,继续。
if (inVPre) {
processRawAttrs(element);
} else if (!element.processed) {
// structural directives
processFor(element);
processIf(element);
processOnce(element);
}
这边是有两个分支,一个是v-pre
环境下的处理,v-pre
环境下只处理html属性。其他环境会处理v-for
、v-if
、v-once
,这边有兴趣的童鞋可以进去看看具体是怎么处理的。
if (!root) {
root = element;
if (process.env.NODE_ENV !== "production") {
checkRootConstraints(root);
}
}
这里是一个check
,判断你在书写template
的时候是不是只有一个根元素,继续。
if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
一个判断,如果不是一元标签(自闭合标签),就将当前的元素赋值给currentParent
,并将当前的元素入栈stack
,这边的stack
的用途和patseHTML
中的stack
用途差不多。
接下来我们来看end
hook。
end
hook end(tag, start, end) {
const element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (process.env.NODE_ENV !== "production" && options.outputSourceRange) {
element.end = end;
}
closeElement(element);
},
先是将stack
出栈,然后还原currentParent
,之后调用closeELement
,这个方法在刚刚start
hook最后一步如果是一元标签时,也会调用。这个方法其实我也理解的不是很透彻,貌似是用来捋清父子关系的,同时还会解析ref
、slot
、component
、事件等。
chars
hook chars(text: string, start: number, end: number) {
if (!currentParent) {
if (process.env.NODE_ENV !== "production") {
if (text === template) {
warnOnce(
"Component template requires a root element, rather than just text.",
{ start }
);
} else if ((text = text.trim())) {
warnOnce(`text "${text}" outside root element will be ignored.`, {
start,
});
}
}
return;
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (
isIE &&
currentParent.tag === "textarea" &&
currentParent.attrsMap.placeholder === text
) {
return;
}
const 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, " ");
}
let res;
let child: ?ASTNode;
if (!inVPre && text !== " " && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text,
};
} else if (
text !== " " ||
!children.length ||
children[children.length - 1].text !== " "
) {
child = {
type: 3,
text,
};
}
if (child) {
if (
process.env.NODE_ENV !== "production" &&
options.outputSourceRange
) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
},
这个hook是用来解析文本节点的。
根节点的判断。
IE的兼容。
使用parseText
方法解析插值语法:{{xx}}
,还顺带解析了filter
。
comment
hook comment(text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true,
};
if (
process.env.NODE_ENV !== "production" &&
options.outputSourceRange
) {
child.start = start;
child.end = end;
}
currentParent.children.push(child);
}
},
为AST创建注释节点,还有一些父子关系,感觉没啥好说的。