[前端] vueparseHTML函数源码解析

2547 0
王子 2022-10-21 15:58:23 | 显示全部楼层 |阅读模式
目录

    正文函数开头定义的一些常量和变量while 循环
      textEnd ===0
    parseStartTag 函数解析开始标签总结:


正文

接上篇:
Vue编译器源码分析AST 抽象语法树
  1. function parseHTML(html, options) {
  2.         var stack = [];
  3.         var expectHTML = options.expectHTML;
  4.         var isUnaryTag$$1 = options.isUnaryTag || no;
  5.         var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
  6.         var index = 0;
  7.         var last, lastTag;
  8.         // 开启一个 while 循环,循环结束的条件是 html 为空,即 html 被 parse 完毕
  9.         while (html) {
  10.                 last = html;
  11.                 if (!lastTag || !isPlainTextElement(lastTag)) {
  12.                         // 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)
  13.                 } else {
  14.                         // parse 的内容是在纯文本标签里 (script,style,textarea)
  15.                 }
  16.         //将整个字符串作为文本对待
  17.                 if (html === last) {
  18.                         options.chars && options.chars(html);
  19.                         if (!stack.length && options.warn) {
  20.                                 options.warn(("Mal-formatted tag at end of template: "" + html + """));
  21.                         }
  22.                         break
  23.                 }
  24.         }
  25.         // Clean up any remaining tags
  26.         parseEndTag();
  27.         function advance(n) {
  28.                 index += n;
  29.                 html = html.substring(n);
  30.         }
  31.         //parse 开始标签
  32.         function parseStartTag() {
  33.                 //...
  34.         }
  35.         //处理 parseStartTag 的结果
  36.         function handleStartTag(match) {
  37.                 //...
  38.         }
  39.         //parse 结束标签
  40.         function parseEndTag(tagName, start, end) {
  41.                 //...
  42.         }
  43. }
复制代码
可以看到 parseHTML 函数接收两个参数:html 和 options ,其中 html 是要被编译的字符串,而options则是编译器所需的选项。
整体上来讲 parseHTML分为三部分。
    函数开头定义的一些常量和变量while 循环parse 过程中需要用到的 analytic function

函数开头定义的一些常量和变量

先从第一部分开始讲起
  1. var stack = [];
  2. var expectHTML = options.expectHTML;
  3. var isUnaryTag$$1 = options.isUnaryTag || no;
  4. var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
  5. var index = 0;
  6. var last, lastTag;
复制代码
第一个变量是 stack,它被初始化为一个空数组,在 while 循环中处理 html 字符流的时候每当遇到一个非单标签,都会将该开始标签 push 到该数组。它的作用模板中 DOM 结构规范性的检测。
但在一个 html 字符串中,如何判断一个非单标签是否缺少结束标签呢?
假设我们有如下html字符串:
  1. <div><p><span></p></div>
复制代码
在编译这个字符串的时候,首先会遇到 div 开始标签,并将该 push 到 stack 数组,然后会遇到 p 开始标签,并将该标签 push 到 stack ,接下来会遇到 span 开始标签,同样被 push 到 stack ,此时 stack 数组内包含三个元素。


再然后便会遇到 p 结束标签,按照正常逻辑可以推理出最先遇到的结束标签,其对应的开始标签应该最后被push到 stack 中,也就是说 stack 栈顶的元素应该是 span ,如果不是 span 而是 p,这说明 span 元素缺少闭合标签。
这就是检测 html 字符串中是否缺少闭合标签的原理。
第二个变量是 expectHTML,它的值被初始化为 options.expectHTML,也就是编译器选项中的 expectHTML。
第三个常量是 isUnaryTag,用来检测一个标签是否是一元标签。
第四个常量是 canBeLeftOpenTag,用来检测一个标签是否是可以省略闭合标签的非一元标签。
    index 初始化为 0 ,标识着当前字符流的读入位置。last 存储剩余还未编译的 html 字符串。lastTag 始终存储着位于 stack 栈顶的元素。

while 循环

接下来将进入第二部分,即开启一个 while 循环,循环的终止条件是 html 字符串为空,即html 字符串全部编译完毕。
  1. while (html) {
  2.         last = html;
  3.         // Make sure we're not in a plaintext content element like script/style
  4.         if (!lastTag || !isPlainTextElement(lastTag)) {
  5.                 var textEnd = html.indexOf('<');
  6.                 if (textEnd === 0) {
  7.                         // Comment:
  8.                         if (comment.test(html)) {
  9.                                 var commentEnd = html.indexOf('-->');
  10.                                 if (commentEnd >= 0) {
  11.                                         if (options.shouldKeepComment) {
  12.                                                 options.comment(html.substring(4, commentEnd));
  13.                                         }
  14.                                         advance(commentEnd + 3);
  15.                                         continue
  16.                                 }
  17.                         }
  18.                         // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
  19.                         if (conditionalComment.test(html)) {
  20.                                 var conditionalEnd = html.indexOf(']>');
  21.                                 if (conditionalEnd >= 0) {
  22.                                         advance(conditionalEnd + 2);
  23.                                         continue
  24.                                 }
  25.                         }
  26.                         // Doctype:
  27.                         var doctypeMatch = html.match(doctype);
  28.                         if (doctypeMatch) {
  29.                                 advance(doctypeMatch[0].length);
  30.                                 continue
  31.                         }
  32.                         // End tag:
  33.                         var endTagMatch = html.match(endTag);
  34.                         if (endTagMatch) {
  35.                                 var curIndex = index;
  36.                                 advance(endTagMatch[0].length);
  37.                                 parseEndTag(endTagMatch[1], curIndex, index);
  38.                                 continue
  39.                         }
  40.                         // Start tag:
  41.                         var startTagMatch = parseStartTag();
  42.                         if (startTagMatch) {
  43.                                 handleStartTag(startTagMatch);
  44.                                 if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  45.                                         advance(1);
  46.                                 }
  47.                                 continue
  48.                         }
  49.                 }
  50.                 var text = (void 0),
  51.                         rest = (void 0),
  52.                         next = (void 0);
  53.                 if (textEnd >= 0) {
  54.                         rest = html.slice(textEnd);
  55.                         while (
  56.                                 !endTag.test(rest) &&
  57.                                 !startTagOpen.test(rest) &&
  58.                                 !comment.test(rest) &&
  59.                                 !conditionalComment.test(rest)
  60.                         ) {
  61.                                 // < in plain text, be forgiving and treat it as text
  62.                                 next = rest.indexOf('<', 1);
  63.                                 if (next < 0) {
  64.                                         break
  65.                                 }
  66.                                 textEnd += next;
  67.                                 rest = html.slice(textEnd);
  68.                         }
  69.                         text = html.substring(0, textEnd);
  70.                         advance(textEnd);
  71.                 }
  72.                 if (textEnd < 0) {
  73.                         text = html;
  74.                         html = '';
  75.                 }
  76.                 if (options.chars && text) {
  77.                         options.chars(text);
  78.                 }
  79.         } else {
  80.                 var endTagLength = 0;
  81.                 var stackedTag = lastTag.toLowerCase();
  82.                 var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag +
  83.                         '[^>]*>)', 'i'));
  84.                 var rest$1 = html.replace(reStackedTag, function(all, text, endTag) {
  85.                         endTagLength = endTag.length;
  86.                         if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
  87.                                 text = text
  88.                                         .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
  89.                                         .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
  90.                         }
  91.                         if (shouldIgnoreFirstNewline(stackedTag, text)) {
  92.                                 text = text.slice(1);
  93.                         }
  94.                         if (options.chars) {
  95.                                 options.chars(text);
  96.                         }
  97.                         return ''
  98.                 });
  99.                 index += html.length - rest$1.length;
  100.                 html = rest$1;
  101.                 parseEndTag(stackedTag, index - endTagLength, index);
  102.         }
  103.         if (html === last) {
  104.                 options.chars && options.chars(html);
  105.                 if (!stack.length && options.warn) {
  106.                         options.warn(("Mal-formatted tag at end of template: "" + html + """));
  107.                 }
  108.                 break
  109.         }
  110. }
复制代码
首先将在每次循环开始时将 html 的值赋给变量 last :
  1. last = html;
复制代码
为什么这么做?在 while 循环即将结束的时候,有一个对 last 和 html 这两个变量的比较,在此可以找到答案:
  1. if (html === last) {}
复制代码
如果两者相等,则说明html 在经历循环体的代码之后没有任何改变,此时会"Mal-formatted tag at end of template: \"" + html + "\"" 错误信息提示。
接下来可以简单看下整体while循环的结构。
  1. while (html) {
  2.   last = html
  3.   if (!lastTag || !isPlainTextElement(lastTag)) {
  4.     // parse 的内容不是在纯文本标签里
  5.   } else {
  6.     // parse 的内容是在纯文本标签里 (script,style,textarea)
  7.   }
  8.   // 极端情况下的处理
  9.   if (html === last) {
  10.     options.chars && options.chars(html)
  11.     if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
  12.       options.warn(`Mal-formatted tag at end of template: "${html}"`)
  13.     }
  14.     break
  15.   }
  16. }
复制代码
接下来我们重点来分析这个if else 中的代码。
  1. !lastTag || !isPlainTextElement(lastTag)
复制代码
lastTag 刚刚讲到它会一直存储 stack 栈顶的元素,但是当编译器刚开始工作时,他只是一个空数组对象,![] == false
isPlainTextElement(lastTag) 检测 lastTag 是否为纯标签内容。
  1. var isPlainTextElement = makeMap('script,style,textarea', true);
复制代码
lastTag 为空数组 ,isPlainTextElement(lastTag ) 返回false, !isPlainTextElement(lastTag) ==true, 有兴趣的同学可以阅读下 makeMap 源码。
接下来我们继续往下看,简化版的代码。
  1. if (!lastTag || !isPlainTextElement(lastTag)) {
  2.   var textEnd = html.indexOf('<')
  3.   if (textEnd === 0) {
  4.     // 第一个字符就是(<)尖括号
  5.   }
  6. var text = (void 0),
  7.      rest = (void 0),
  8.      next = (void 0);
  9.   if (textEnd >= 0) {
  10.     //第一个字符不是(<)尖括号
  11.   }
  12.   if (textEnd < 0) {
  13.     // 第一个字符不是(<)尖括号
  14.   }
  15.   if (options.chars && text) {
  16.     options.chars(text)
  17.   }
  18. } else {
  19.   // 省略 ...
  20. }
复制代码
textEnd ===0

当 textEnd === 0 时,说明 html 字符串的第一个字符就是左尖括号,比如 html 字符串为:<div>box</div>,那么这个字符串的第一个字符就是左尖括号(<)。
  1. if (textEnd === 0) {
  2.         // Comment: 如果是注释节点
  3.         if (comment.test(html)) {
  4.                 var commentEnd = html.indexOf('-->');
  5.                 if (commentEnd >= 0) {
  6.                         if (options.shouldKeepComment) {
  7.                                 options.comment(html.substring(4, commentEnd));
  8.                         }
  9.                         advance(commentEnd + 3);
  10.                         continue
  11.                 }
  12.         }
  13.         //如果是条件注释节点
  14.         if (conditionalComment.test(html)) {
  15.                 var conditionalEnd = html.indexOf(']>');
  16.                 if (conditionalEnd >= 0) {
  17.                         advance(conditionalEnd + 2);
  18.                         continue
  19.                 }
  20.         }
  21.         // 如果是 Doctyp节点
  22.         var doctypeMatch = html.match(doctype);
  23.         if (doctypeMatch) {
  24.                 advance(doctypeMatch[0].length);
  25.                 continue
  26.         }
  27.         // End tag:  结束标签
  28.         var endTagMatch = html.match(endTag);
  29.         if (endTagMatch) {
  30.                 var curIndex = index;
  31.                 advance(endTagMatch[0].length);
  32.                 parseEndTag(endTagMatch[1], curIndex, index);
  33.                 continue
  34.         }
  35.         // Start tag: 开始标签
  36.         var startTagMatch = parseStartTag();
  37.         if (startTagMatch) {
  38.                 handleStartTag(startTagMatch);
  39.                 if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  40.                         advance(1);
  41.                 }
  42.                 continue
  43.         }
  44. }
复制代码
细枝末节我们不看,重点在End tag 、 Start tag 上。
我们先从解析标签开始分析
  1. var startTagMatch = parseStartTag();
  2. if (startTagMatch) {
  3.         handleStartTag(startTagMatch);
  4.         if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  5.                 advance(1);
  6.         }
  7.         continue
  8. }
复制代码
parseStartTag 函数解析开始标签

解析开始标签会调用parseStartTag函数,如果有返回值,说明开始标签解析成功。
  1. function parseStartTag() {
  2.         var start = html.match(startTagOpen);
  3.         if (start) {
  4.                 var match = {
  5.                         tagName: start[1],
  6.                         attrs: [],
  7.                         start: index
  8.                 };
  9.                 advance(start[0].length);
  10.                 var end, attr;
  11.                 while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  12.                         advance(attr[0].length);
  13.                         match.attrs.push(attr);
  14.                 }
  15.                 if (end) {
  16.                         match.unarySlash = end[1];
  17.                         advance(end[0].length);
  18.                         match.end = index;
  19.                         return match
  20.                 }
  21.         }
  22. }
复制代码
parseStartTag 函数首先会调用 html 字符串的 match 函数匹配 startTagOpen 正则,前面我们分析过编译器所需的正则。
Vue编译器token解析规则-正则分析
如果匹配成功,那么start 将是一个包含两个元素的数组:第一个元素是标签的开始部分(包含< 和 标签名称);第二个元素是捕获组捕获到的标签名称。比如有如下template:
  1. <div></div>
复制代码
start为:
  1. start = ['<div', 'div']
复制代码
接下来:
定义了 match 变量,它是一个对象,初始状态下拥有三个属性:
    tagName:它的值为 start[1] 即标签的名称。attrs :这个数组就是用来存储将来被匹配到的属性。start:初始值为 index,是当前字符流读入位置在整个 html 字符串中的相对位置。
  1. advance(start[0].length);
复制代码
相对就比较简单了,他的作用就是在源字符中截取已经编译完成的字符,我们知道当html 字符为 “”,整个词法分析的工作就结束了,在这中间扮演重要角色的就是advance方法。
  1. function advance(n) {
  2.         index += n;
  3.         html = html.substring(n);
  4. }
复制代码
接下来:
  1. var end, attr;
  2. while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  3.         advance(attr[0].length);
  4.         match.attrs.push(attr);
  5. }
  6. if (end) {
  7.         match.unarySlash = end[1];
  8.         advance(end[0].length);
  9.         match.end = index;
  10.         return match
  11.   }
  12. }
复制代码
主要看while循环,循环的条件有两个,第一个条件是:没有匹配到开始标签的结束部分,这个条件的实现方式主要使用了 startTagClose 正则,并将结果保存到 end 变量中。
第二个条件是:匹配到了属性,主要使用了attribute正则。
总结下这个while循环成立要素:没有匹配到开始标签的结束部分,并且匹配到了开始标签中的属性,这个时候循环体将被执行,直到遇到开始标签的结束部分为止。
接下来在循环体内做了两件事,首先调用advance函数,参数为attr[0].length即整个属性的长度。然后会将此次循环匹配到的结果push到前面定义的match对象的attrs数组中。
  1. advance(attr[0].length);
  2. match.attrs.push(attr);
复制代码
接下来看下最后这部分代码。
  1. if (end) {
  2.         match.unarySlash = end[1];
  3.         advance(end[0].length);
  4.         match.end = index;
  5.         return match
  6. }
复制代码
首先判断了变量 end 是否为真,我们知道,即使匹配到了开始标签的开始部分以及属性部分但是却没有匹配到开始标签的结束部分,这说明这根本就不是一个开始标签。所以只有当变量end存在,即匹配到了开始标签的结束部分时,才能说明这是一个完整的开始标签。
如果变量end的确存在,那么将会执行 if 语句块内的代码,不过我们需要先了解一下变量end的值是什么?
比如当html(template)字符串如下时:
<br />
那么匹配到的end的值为:
end = ['/>', '/']
比如当html(template)字符串如下时:
<div></div>
那么匹配到的end的值为:
end = ['>', undefined]
结论如果end[1]不为undefined,那么说明该标签是一个一元标签。
那么现在再看if语句块内的代码,将很容易理解,首先在match对象上添加unarySlash属性,其值为end[1]
  1. match.unarySlash = end[1];
复制代码
然后调用advance函数,参数为end[0].length,接着在match 对象上添加了一个end属性,它的值为index,注意由于先调用的advance函数,所以此时的index已经被更新了。最后将match 对象作为 parseStartTag 函数的返回值返回。
只有当变量end存在时,即能够确定确实解析到了一个开始标签的时候parseStartTag函数才会有返回值,并且返回值是match对象,其他情况下parseStartTag全部返回undefined。

总结:

我们模拟假设有如下html(template)字符串:
  1. <div id="box" v-if="watings"></div>
复制代码
则parseStartTag函数的返回值如下:
  1. match = {
  2.   tagName: 'div',
  3.   attrs: [
  4.     [
  5.       'id="box"',
  6.       'id',
  7.       '=',
  8.       'box',
  9.       undefined,
  10.       undefined
  11.     ],
  12.     [
  13.       ' v-if="watings"',
  14.       'v-if',
  15.       '=',
  16.       'watings',
  17.       undefined,
  18.       undefined
  19.     ]
  20.   ],
  21.   start: index,
  22.   unarySlash: undefined,
  23.   end: index
  24. }
复制代码
我们讲解完了parseStartTag函数及其返回值,现在我们回到对开始标签的 parse 部分,接下来我们会继续讲解,拿到返回值之后的处理。
  1. var startTagMatch = parseStartTag();
  2. if (startTagMatch) {
  3.         handleStartTag(startTagMatch);
  4.         if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
  5.                 advance(1);
  6.         }
  7.         continue
  8. }
复制代码
篇幅有限请移步:
parseHTML 函数源码解析返回值后的处理
以上就是vue parseHTML 函数源码解析的详细内容,更多关于vue parseHTML函数的资料请关注中国红客联盟其它相关文章!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行