上一节到了parseHTML函数,该函数接受一个字符串与一个对象,字符串即对应的DOM,对象包含几个字符串匹配集及3个长函数。
简略梳理部分函数代码如下:
// Line-7672
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) {
last = html;
// 排除script,style,textarea三个标签
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// 截取注释
if (comment.test(html)) {
var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) {
advance(commentEnd + 3);
continue
}
}
// 处理向下兼容的注释 比如说<!--[if lt IE 9]>
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>'); if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}
// Doctype:
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue
}
// Start tag:
// 匹配起始标签
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
}
// 初始化为undefined 这样安全且字符数少一点
var text = (void 0),
rest$1 = (void 0),
next = (void 0);
if (textEnd >= 0) {
rest$1 = html.slice(textEnd);
while (!endTag.test(rest$1) &&
!startTagOpen.test(rest$1) &&
!comment.test(rest$1) &&
!conditionalComment.test(rest$1)
) {
// 处理文本中的<字符
next = rest$1.indexOf('<', 1);
if (next < 0) {
break
}
textEnd += next;
rest$1 = html.slice(textEnd);
}
text = html.substring(0, textEnd);
advance(textEnd);
} if (textEnd < 0) {
text = html;
html = '';
} if (options.chars && text) {
options.chars(text);
}
} else {
/* code... */
} if (html === last) {
/* code... */
}
}
// Clean up any remaining tags
parseEndTag(); function advance(n) {
/* code... */
} function parseStartTag() {
/* code... */
} function handleStartTag(match) {
/* code... */
} function parseEndTag(tagName, start, end) {
/* code... */
}
}
函数比较长,除去开头的参数获取,后面直接用while循环开始对字符串进行切割。
在判断标签不是script,style,textarea三个特殊标签后,当字符串以<开头时,以注释、向下兼容注释、Doctype、结束标签、起始标签的顺序依次切割。
由于案例中字符串是以<div开头,所以直接跳到起始标签的匹配:
// Line-7722
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
} // Line-7795
function parseStartTag() {
// 正则匹配
var start = html.match(startTagOpen);
if (start) {
var match = {
// 标签名(div)
tagName: start[1],
// 属性
attrs: [],
// 游标索引(初始为0)
start: index
};
advance(start[0].length);
var end, attr;
// 进行属性的正则匹配
// startTagClose匹配/>或>
// attribute匹配属性 正则太长 没法讲
// 本例中attr匹配后 => ['id=app','id','=','app']
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
// 属性加入
match.attrs.push(attr);
}
// 在第二次while循环后 end匹配到结束标签 => ['>','']
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
// 标记结束位置
match.end = index;
// 返回匹配对象
return match
}
}
} // Line-7790
// 该函数将函数局部变量index往前推 并切割字符串
function advance(n) {
index += n;
html = html.substring(n);
}
可以看到,通过起始标签的匹配,字符串的<div id='app'>已经被切割出来,保存在一个对象中返回:
接下来,会调用handleStartTag方法再次处理返回的对象,看一下这个方法:
// Line-7818
function handleStartTag(match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash; if (expectHTML) {
// PhrasingTag(段落元素)涉及到标签元素类型 具体可见http://www.5icool.org/a/201308/a2081.html
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);
}
// 可以省略闭合标签
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
} // 自闭合标签
var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash; // 记录属性个数 目前只有一个id属性
var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
// 一个bug 在对(.)?匹配时会出现
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') {
delete args[3];
}
if (args[4] === '') {
delete args[4];
}
if (args[5] === '') {
delete args[5];
}
}
// 匹配属性名app
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
// 处理转义字符
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
};
}
// 将切割出来的字符串转换为AST
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs
});
// 标记结束标签
lastTag = tagName;
} // 这是参数中第一个函数
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
} // Line-7667
function decodeAttr(value, shouldDecodeNewlines) {
// lg,gt等字符的正则
var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
return value.replace(re, function(match) {
return decodingMap[match];
})
}
在该函数中,对之前的对象进行了二次处理,根据标签名、属性生成一个新对象,push到最开始的stack数组中,结果如图所示:
由于匹配的是起始标签,所以也会以这个标签名结束,因此被标记为最后的结束标签,即前面一直是undefined的lastTag。
最后,调用了一个start函数,蒙了半天没找到,后来才发现是最开始传进来的参数中有3个函数:start、end、chars,现在可以看一下这个方法干啥用的了。
start方法只接受3个参数,这里传了5个,后面2个被忽略,参数情况是这样的:
这个方法比较长:
// Line-8026
function start(tag, attrs, unary) {
// 检查命名空间是否是svg或者math
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag); // handle IE svg bug
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
} var element = {
type: 1,
tag: tag,
// {name:'id',value:'app'}
attrsList: attrs,
// {id:'app'}
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: []
};
if (ns) {
element.ns = ns;
}
// 检查tag属性是否是style、script
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
/* warning */
} // apply pre-transforms 本例中没有
// Line-7990:preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
for (var i = 0; i < preTransforms.length; i++) {
preTransforms[i](element, options);
} if (!inVPre) {
// 判断是否有v-pre属性
processPre(element);
if (element.pre) {
inVPre = true;
}
}
// 判断tag是不是pre
if (platformIsPreTag(element.tag)) {
inPre = true;
}
// 分支跳转到else
if (inVPre) {
processRawAttrs(element);
} else {
// 处理v-for
processFor(element);
// 处理v-if,v-else,v-else-if
processIf(element);
// 处理v-once
processOnce(element);
// 处理:
processKey(element); // 检测是否是空属性节点
element.plain = !element.key && !attrs.length; // 处理:ref或v-bind:ref属性
processRef(element);
// 当tag为slot时
processSlot(element);
// 处理:is或v-bind:is属性
processComponent(element);
// Line-7991:transforms = pluckModuleFunction(options.modules, 'transformNode');
// 处理class与style属性 包括原始的和通过:动态绑定
for (var i$1 = 0; i$1 < transforms.length; i$1++) {
transforms[i$1](element, options);
}
// 处理属性
processAttrs(element);
} // 根元素不允许为slot或template 且不能有v-for属性
// 总之必须为单一不可变的节点
function checkRootConstraints(el) {
{
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
"Cannot use <" + (el.tag) + "> as component root element because it may " +
'contain multiple nodes.'
);
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.'
);
}
}
} // tree management
// 这个root是在parse函数开始的时候定义的
if (!root) {
root = element;
checkRootConstraints(root);
} else if (!stack.length) {
// 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."
);
}
}
// 没有父元素 跳过
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else if (element.slotScope) { // scoped slot
currentParent.plain = false;
var name = element.slotTarget || '"default"';
(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
if (!unary) {
currentParent = element;
stack.push(element);
} else {
endPre(element);
}
// 没有这个 跳过
// Line-7992:postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
postTransforms[i$2](element, options);
}
} // Line-8470
// 函数作用:attrs={name:'id',value:'app'} => map = {id : app}
function makeAttrsMap(attrs) {
var map = {};
for (var i = 0, l = attrs.length; i < l; i++) {
// 检测重复属性名
if (
"development" !== 'production' &&
map[attrs[i].name] && !isIE && !isEdge
) {
warn$2('duplicate attribute: ' + attrs[i].name);
}
map[attrs[i].name] = attrs[i].value;
}
return map
}
这个方法首先对标签名进行校验,然后再对属性进行更细致的处理,比如说v-pre,v-for,v-if等等,案例里没有,所以暂时不分析如何处理,下次再搞。
值得注意的是transforms这个数组,包含两个函数:transformNode与transformNode$1,其实只是对静态或动态绑定的class与style进行处理,代码如下:
// Line-9416
function transformNode(el, options) {
var warn = options.warn || baseWarn;
// 获取原始class属性
var staticClass = getAndRemoveAttr(el, 'class');
if ("development" !== 'production' && staticClass) {
var expression = parseText(staticClass, options.delimiters);
if (expression) {
/*<div class="{{ val }}"> => <div :class="val"> */
}
}
// 将原始class属性保存为属性
if (staticClass) {
el.staticClass = JSON.stringify(staticClass);
}
// 获取:class
var classBinding = getBindingAttr(el, 'class', false /* getStatic */ );
if (classBinding) {
el.classBinding = classBinding;
}
} // Line-9458
function transformNode$1(el, options) {
var warn = options.warn || baseWarn;
var staticStyle = getAndRemoveAttr(el, 'style');
if (staticStyle) {
/* istanbul ignore if */
{
var expression = parseText(staticStyle, options.delimiters);
if (expression) {
/*<div style="{{ val }}"> => <div :style="val"> */
}
}
el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
} var styleBinding = getBindingAttr(el, 'style', false /* getStatic */ );
if (styleBinding) {
el.styleBinding = styleBinding;
}
}
在最后,调用processAttrs对动态绑定的属性(v-,@,:)进行处理,代码如下:
// Line-8376
function processAttrs(el) {
// {name:'id',value:'app'}
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) {
// id
name = rawName = list[i].name;
// app
value = list[i].value;
// dirRE => 以v-、@、:开头
if (dirRE.test(name)) {
// 标记为拥有动态绑定属性 本例中没有 跳过……
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '');
value = parseFilters(value);
isProp = false;
if (modifiers) {
if (modifiers.prop) {
isProp = true;
name = camelize(name);
if (name === 'innerHtml') {
name = 'innerHTML';
}
}
if (modifiers.camel) {
name = camelize(name);
}
if (modifiers.sync) {
addHandler(
el,
("update:" + (camelize(name))),
genAssignmentCode(value, "$event")
);
}
}
if (isProp || platformMustUseProp(el.tag, el.attrsMap.type, name)) {
addProp(el, name, value);
} else {
addAttr(el, name, value);
}
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '');
addHandler(el, name, value, modifiers, false, warn$2);
} else { // normal directives
name = name.replace(dirRE, '');
// parse arg
var argMatch = name.match(argRE);
var arg = argMatch && argMatch[1];
if (arg) {
name = name.slice(0, -(arg.length + 1));
}
addDirective(el, name, rawName, value, arg, modifiers);
if ("development" !== 'production' && name === 'model') {
checkForAliasModel(el, value);
}
}
} else {
{
var expression = parseText(value, delimiters);
if (expression) {
/* warn */
}
}
// 添加了个attrs属性
addAttr(el, name, JSON.stringify(value));
}
}
}
到此,element的属性如图所示:,attrs和attrsList是一样的,都是一个数组,包含一个对象,值为{name:id,value:app}。
这函数有点长。