vue parseHTML 函數源碼解析

正文

接上篇:

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><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 字符串全部編譯完畢。

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]*?)(</' + stackedTag +
			'[^>]*>)', 'i'));
		var rest$1 = html.replace(reStackedTag, function(all, text, endTag) {
			endTagLength = endTag.length;
			if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
				text = text
					.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
					.replace(/<!\[CDATA\[([\s\S]*?)]]>/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 字符串為:<div>box</div>,那麼這個字符串的第一個字符就是左尖括號(<)。

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)) &amp;&amp; (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:

<div></div>

start為:

start = ['&lt;div', 'div']

接下來:

定義瞭 match 變量,它是一個對象,初始狀態下擁有三個屬性:

  • tagName:它的值為 start[1] 即標簽的名稱。
  • attrs :這個數組就是用來存儲將來被匹配到的屬性。
  • start:初始值為 index,是當前字符流讀入位置在整個 html 字符串中的相對位置。
advance(start[0].length);

相對就比較簡單瞭,他的作用就是在源字符中截取已經編譯完成的字符,我們知道當html 字符為 “”,整個詞法分析的工作就結束瞭,在這中間扮演重要角色的就是advance方法。

function advance(n) {
	index += n;
	html = html.substring(n);
}

接下來:

var end, attr;
while (!(end = html.match(startTagClose)) &amp;&amp; (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
  }
}

主要看while循環,循環的條件有兩個,第一個條件是:沒有匹配到開始標簽的結束部分,這個條件的實現方式主要使用瞭 startTagClose 正則,並將結果保存到 end 變量中。

第二個條件是:匹配到瞭屬性,主要使用瞭attribute正則。

總結下這個while循環成立要素:沒有匹配到開始標簽的結束部分,並且匹配到瞭開始標簽中的屬性,這個時候循環體將被執行,直到遇到開始標簽的結束部分為止。

接下來在循環體內做瞭兩件事,首先調用advance函數,參數為attr[0].length即整個屬性的長度。然後會將此次循環匹配到的結果push到前面定義的match對象的attrs數組中。

advance(attr[0].length);
match.attrs.push(attr);

接下來看下最後這部分代碼。

if (end) {
	match.unarySlash = end[1];
	advance(end[0].length);
	match.end = index;
	return match
}

首先判斷瞭變量 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]

match.unarySlash = end[1];

然後調用advance函數,參數為end[0].length,接著在match 對象上添加瞭一個end屬性,它的值為index,註意由於先調用的advance函數,所以此時的index已經被更新瞭。最後將match 對象作為 parseStartTag 函數的返回值返回。

隻有當變量end存在時,即能夠確定確實解析到瞭一個開始標簽的時候parseStartTag函數才會有返回值,並且返回值是match對象,其他情況下parseStartTag全部返回undefined。

總結:

我們模擬假設有如下html(template)字符串:

<div id="box" v-if="watings"></div>

則parseStartTag函數的返回值如下:

match = {
  tagName: 'div',
  attrs: [
    [
      'id="box"',
      'id',
      '=',
      'box',
      undefined,
      undefined
    ],
    [
      ' v-if="watings"',
      'v-if',
      '=',
      'watings',
      undefined,
      undefined
    ]
  ],
  start: index,
  unarySlash: undefined,
  end: index
}

我們講解完瞭parseStartTag函數及其返回值,現在我們回到對開始標簽的 parse 部分,接下來我們會繼續講解,拿到返回值之後的處理。

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

篇幅有限請移步:

parseHTML 函數源碼解析返回值後的處理

以上就是vue parseHTML 函數源碼解析的詳細內容,更多關於vue parseHTML函數的資料請關註WalkonNet其它相關文章!

推薦閱讀: