.7-Vue源码之AST(3)
上一节到了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}。
这函数有点长。