您现在的位置是:网站首页> 编程资料编程资料

vue parseHTML 函数源码解析_vue.js_

2023-05-24 431人已围观

简介 vue parseHTML 函数源码解析_vue.js_

正文

接上篇:

Vue编译器源码分析AST 抽象语法树

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 被 parse 完毕 while (html) { last = html; if (!lastTag || !isPlainTextElement(lastTag)) { // 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea) } else { // parse 的内容是在纯文本标签里 (script,style,textarea) } //将整个字符串作为文本对待 if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\"")); } break } } // Clean up any remaining tags parseEndTag(); function advance(n) { index += n; html = html.substring(n); } //parse 开始标签 function parseStartTag() { //... } //处理 parseStartTag 的结果 function handleStartTag(match) { //... } //parse 结束标签 function parseEndTag(tagName, start, end) { //... } } 

可以看到 parseHTML 函数接收两个参数:html 和 options ,其中 html 是要被编译的字符串,而options则是编译器所需的选项。

整体上来讲 parseHTML分为三部分。

  • 函数开头定义的一些常量和变量
  • while 循环
  • parse 过程中需要用到的 analytic function

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

先从第一部分开始讲起

var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; var index = 0; var last, lastTag; 

第一个变量是 stack,它被初始化为一个空数组,在 while 循环中处理 html 字符流的时候每当遇到一个非单标签,都会将该开始标签 push 到该数组。它的作用模板中 DOM 结构规范性的检测。

但在一个 html 字符串中,如何判断一个非单标签是否缺少结束标签呢?

假设我们有如下html字符串:

在编译这个字符串的时候,首先会遇到 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 字符串全部编译完毕。

while (html) { last = html; // Make sure we're not in a plaintext content element like script/style if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<'); if (textEnd === 0) { // Comment: if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)); } advance(commentEnd + 3); continue } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment 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); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } } var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { rest = html.slice(textEnd); 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); advance(textEnd); } if (textEnd < 0) { text = html; html = ''; } if (options.chars && text) { options.chars(text); } } else { var endTagLength = 0; var stackedTag = lastTag.toLowerCase(); var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(]*>)', 'i')); var rest$1 = html.replace(reStackedTag, function(all, text, endTag) { endTagLength = endTag.length; if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { text = text .replace(//g, '$1') // #7298 .replace(//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); } if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\"")); } break } } 

首先将在每次循环开始时将 html 的值赋给变量 last :

last = html; 

为什么这么做?在 while 循环即将结束的时候,有一个对 last 和 html 这两个变量的比较,在此可以找到答案:

if (html === last) {} 

如果两者相等,则说明html 在经历循环体的代码之后没有任何改变,此时会"Mal-formatted tag at end of template: \"" + html + "\"" 错误信息提示。

接下来可以简单看下整体while循环的结构。

while (html) { last = html if (!lastTag || !isPlainTextElement(lastTag)) { // parse 的内容不是在纯文本标签里 } else { // parse 的内容是在纯文本标签里 (script,style,textarea) } // 极端情况下的处理 if (html === last) { options.chars && options.chars(html) if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) { options.warn(`Mal-formatted tag at end of template: "${html}"`) } break } } 

接下来我们重点来分析这个if else 中的代码。

!lastTag || !isPlainTextElement(lastTag) 

lastTag 刚刚讲到它会一直存储 stack 栈顶的元素,但是当编译器刚开始工作时,他只是一个空数组对象,![] == false

isPlainTextElement(lastTag) 检测 lastTag 是否为纯标签内容。

var isPlainTextElement = makeMap('script,style,textarea', true); 

lastTag 为空数组 ,isPlainTextElement(lastTag ) 返回false, !isPlainTextElement(lastTag) ==true, 有兴趣的同学可以阅读下 makeMap 源码。

接下来我们继续往下看,简化版的代码。

if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<') if (textEnd === 0) { // 第一个字符就是(<)尖括号 } var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { //第一个字符不是(<)尖括号 } if (textEnd < 0) { // 第一个字符不是(<)尖括号 } if (options.chars && text) { options.chars(text) } } else { // 省略 ... } 

textEnd ===0

当 textEnd === 0 时,说明 html 字符串的第一个字符就是左尖括号,比如 html 字符串为:

box
,那么这个字符串的第一个字符就是左尖括号(<)。

if (textEnd === 0) { // Comment: 如果是注释节点 if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)); } advance(commentEnd + 3); continue } } //如果是条件注释节点 if (conditionalComment.test(html)) { var conditionalEnd = html.indexOf(']>'); if (conditionalEnd >= 0) { advance(conditionalEnd + 2); continue } } // 如果是 Doctyp节点 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); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } } 

细枝末节我们不看,重点在End tag 、 Start tag 上。

我们先从解析标签开始分析

var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } 

parseStartTag 函数解析开始标签

解析开始标签会调用parseStartTag函数,如果有返回值,说明开始标签解析成功。

function parseStartTag() { 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(attribute))) { advance(attr[0].length); match.attrs.push(attr); } if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match } } } 

parseStartTag 函数首先会调用 html 字符串的 match 函数匹配 startTagOpen 正则,前面我们分析过编译器所需的正则。

Vue编译器token解析规则-正则分析

如果匹配成功,那么start 将是一个包含两个元素的数组:第一个元素是标签的开始部分(包含< 和 标签名称);第二个元素是捕获组捕获到的标签名称。比如有如下template:

start为:

start = ['

-六神源码网