vue parseHTML函數解析器遇到結束標簽

引言

承接上篇 parseHTML 函數源碼解析

接下來我們將會講解當 textEnd === 0 解析器遇到結束標簽,parse 結束標簽的代碼如下:

// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
	var curIndex = index;
	advance(endTagMatch[0].length);
	parseEndTag(endTagMatch[1], curIndex, index);
	continue
}

match函數匹配正則endTag

首先調用 html 字符串的 match 函數匹配正則 endTag ,將結果保存在常量endTagMatch中。正則 endTag 用來匹配結束標簽,並且擁有一個捕獲組用來捕獲標簽名字,比如有如下html 字符串:

<div></div>

endTagMatch 輸出如下:

endTagMatch = [
  '</div>',
  'div'
]

第一個元素是整個匹配到的結束標簽字符串

第二個元素是對應的標簽名字。

如果匹配成功 if 語句塊的代碼將被執行,首先使用 curIndex 常量存儲當前 index 的值,然後調用 advance 函數,並以 endTagMatch[0].length 作為參數,接著調用瞭 parseEndTag 函數對結束標簽進行解析,傳遞給 parseEndTag 函數的三個參數分別是:標簽名以及結束標簽在 html 字符串中起始和結束的位置,最後調用 continue 語句結束此次循環。

關鍵 parseEndTag 函數代碼

現在我們來講解下關鍵 parseEndTag 函數代碼如下:

function parseEndTag(tagName, start, end) {
	var pos, lowerCasedTagName;
	if (start == null) {
		start = index;
	}
	if (end == null) {
		end = index;
	}
	// Find the closest opened tag of the same type
	if (tagName) {
		lowerCasedTagName = tagName.toLowerCase();
		for (pos = stack.length - 1; pos >= 0; pos--) {
			if (stack[pos].lowerCasedTag === lowerCasedTagName) {
				break
			}
		}
	} else {
		// If no tag name is provided, clean shop
		pos = 0;
	}
	if (pos >= 0) {
		// Close all the open elements, up the stack
		for (var i = stack.length - 1; i >= pos; i--) {
			if (i > pos || !tagName &&
				options.warn
			) {
				options.warn(
					("tag <" + (stack[i].tag) + "> has no matching end tag.")
				);
			}
			if (options.end) {
				options.end(stack[i].tag, start, end);
			}
		}
		// Remove the open elements from the stack
		stack.length = pos;
		lastTag = pos && stack[pos - 1].tag;
	} else if (lowerCasedTagName === 'br') {
		if (options.start) {
			options.start(tagName, [], true, start, end);
		}
	} else if (lowerCasedTagName === 'p') {
		if (options.start) {
			options.start(tagName, [], false, start, end);
		}
		if (options.end) {
			options.end(tagName, start, end);
		}
	}
}

你需要知道 parseEndTag 函數調用之前已經獲得到瞭結束標簽的名字以及結束標簽在html(template)字符串中的起始和結束位置。 但是這並不代表著 html parser 結束瞭。

為什麼?

還記得我們之前講的 stack 棧嗎? 之前我們講到通過stack可以檢測是否有非一元標簽是否微寫閉合標簽,接下來還會處理 stack 棧中剩餘的標簽。

除瞭這些功能之外,parseEndTag函數還會做一件事兒,如果你感興趣你可以在任何html文件中寫下如下內容:

<body>
  </br>
  </p>
</body>

上面的html片段中,我們分別寫瞭</br>、</p>的結束標簽,但註意我們並沒有寫起始標簽,然後瀏覽器是能夠正常解析他們的,其中 </br> 標簽被正常解析為 <br> 標簽,而</p>標簽被正常解析為 <p></p> 。除瞭 br 與 p 其他任何標簽如果你隻寫瞭結束標簽那麼瀏覽器都將會忽略。所以為瞭與瀏覽器的行為相同,parseEndTag 函數也需要專門處理br與p的結束標簽,即:</br> 和</p>。

總結parseEndTag 函數作用

  • 檢測是否缺少閉合標簽
  • 處理 stack 棧中剩餘的標簽
  • 解析</br> 與標簽,與瀏覽器的行為相同

當一個函數擁有兩個及以上功能的時候,最常用的技巧就是通過參數進行控制,還記得jQuery中的Access 嗎?parseEndTag 函數接收三個參數,這三個參數其實都是可選的,根據傳參的不同其功能也不同。

  • 第一種是處理普通的結束標簽,此時三個參數都傳遞
  • 第二種是隻傳遞第一個參數
  • 第三種是不傳遞參數,處理 stack 棧剩餘未處理的標簽。

代碼並不復雜我們一起來看下吧!

var pos, lowerCasedTagName;
if (start == null) {
	start = index;
}
if (end == null) {
	end = index;
}

定瞭兩個變量:pos和 lowerCasedTagName,其中變量 pos 會在後面用於判斷 html 字符串是否缺少結束標簽,lowerCasedTagName 變量用來存儲 tagName 的小寫版。

接著是兩句if 語句,當 start 和 end 不存在時,將這兩個變量的值設置為當前字符流的讀入位置,即index。

所以當我們看到這兩個 if 語句時,我們就應該能夠想到:parseEndTag 函數的第二個參數和第三個參數都是可選的。

其實這種使用 parseEndTag 函數的方式我們在handleStartTag 函數中見過,當時我們沒有對其進行講解一起來回顧下。

if (expectHTML) {
  if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
    parseEndTag(lastTag)
  }
  if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
    parseEndTag(tagName)
  }
}

我們知道 lastTag 引用的是stack棧頂的元素,也就是最近(或者說上一次)遇到的開始標簽,所以如下判斷條件:

lastTag === 'p' && isNonPhrasingTag(tagName)

這裡想表達的意思是:最近一次遇到的開始標簽是 p 標簽,並且當前正在解析的開始標簽必須不能是段落式內容(Phrasing content)模型,這時候 if 語句塊的代碼才會執行,即調用parseEndTag(lastTag)。

首先大傢要知道每一個 html 元素都擁有一個或多個內容模型(content model),其中p 標簽本身的內容模型是流式內容(Flow content),並且 p 標簽的特性是隻允許包含段落式內容(Phrasing content)。

所以條件成立的情況如下:

<p><h1></h1></p>

 

在解析上面這段 html 字符串的時候,首先遇到p標簽的開始標簽,此時lastTag被設置為 p ,緊接著會遇到 h1 標簽的開始標簽,由於 h2 標簽的內容模型屬於非段落式內容(Phrasing content)模型,所以會立即調用 parseEndTag(lastTag) 函數閉合 p 標簽,此時由於強行插入瞭</p> 標簽,所以解析後的字符串將變為如下內容:

<p></p><h2></h2></p>

接著,繼續解析該字符串,會遇到 <h2></h2> 標簽並正常解析之,最後解析器會遇到一個單獨的p 標簽的結束標簽,即:</p>。

這個時候就回到瞭我們前面講過的,當解析器遇到 p 標簽或者 br 標簽的結束標簽時會補全他們,最終<p><h2></h2></p> 這段 html 字符串將被解析為:

<p></p><h2></h2><p></p>

而這也就是瀏覽器的行為,以上是第一個if 分支的意義。還有第二個if分支,它的條件如下:

canBeLeftOpenTag(tagName) && lastTag === tagName

以上條件成立的意思是:當前正在解析的標簽是一個可以省略結束標簽的標簽,並且與上一次解析到的開始標簽相同,如下:

<p>max
<p>kaixin

p 標簽是可以省略結束標簽的標簽,所以當解析到一個p標簽的開始標簽並且下一次遇到的標簽也是p標簽的開始標簽時,會立即關閉第二個p標簽。即調用:parseEndTag(tagName) 函數,然後由於第一個p標簽缺少閉合標簽所以會Vue會給你一個警告。

handleStartTag函數後續

接下來我們繼續講解handleStartTag函數後續的內容。

if (tagName) {
	lowerCasedTagName = tagName.toLowerCase();
	for (pos = stack.length - 1; pos &gt;= 0; pos--) {
		if (stack[pos].lowerCasedTag === lowerCasedTagName) {
			break
		}
	}
} else {
	// If no tag name is provided, clean shop
	pos = 0;
}

如果tagName存在,lowerCasedTagName 獲取的是 tagName 小寫之後的值,接下來開啟一個 for 循環從後向前遍歷 stack 棧,直到找到相應的位置,並且該位置索引會保存到 pos 變量中,如果 tagName 不存在,則直接將 pos 設置為 0 。

開頭我們講到 pos 變量會被用來判斷是否有元素缺少閉合標簽。怎麼做到的呢?看完下面的代碼你就明白瞭。

if (pos >= 0) {
	// Close all the open elements, up the stack
	for (var i = stack.length - 1; i >= pos; i--) {
		if (i > pos || !tagName &&
			options.warn
		) {
			options.warn(
				("tag <" + (stack[i].tag) + "> has no matching end tag.")
			);
		}
		if (options.end) {
			options.end(stack[i].tag, start, end);
		}
	}
	// Remove the open elements from the stack
	stack.length = pos;
	lastTag = pos && stack[pos - 1].tag;
} else if (lowerCasedTagName === 'br') {
	if (options.start) {
		options.start(tagName, [], true, start, end);
	}
} else if (lowerCasedTagName === 'p') {
	if (options.start) {
		options.start(tagName, [], false, start, end);
	}
	if (options.end) {
		options.end(tagName, start, end);
	}
}

上面代碼由三部分組成,即if…else if…else if。首先我們查看 if 語句塊,當 pos >= 0 的時候就會走 if 語句塊。在 if 語句塊內開啟一個 for 循環,同樣是從後向前遍歷 stack 數組,如果發現 stack 數組中存在索引大於 pos 的元素,那麼該元素一定是缺少閉合標簽的,這個時候如果是在非生產環境那麼 Vue 便會打印一句警告,告訴你缺少閉合標簽。除瞭打印一句警告之外,隨後會調用 options.end(stack[i].tag, start, end) 立即將其閉合,這是為瞭保證解析結果的正確性。

最後更新 stack 棧以及 lastTag

stack.length = pos;
lastTag = pos && stack[pos - 1].tag;

瞭解下剩下的兩個else if:

if (pos >= 0) {
  // ... 省略
} else if (lowerCasedTagName === 'br') {
  if (options.start) {
    options.start(tagName, [], true, start, end)
  }
} else if (lowerCasedTagName === 'p') {
  if (options.start) {
    options.start(tagName, [], false, start, end)
  }
  if (options.end) {
    options.end(tagName, start, end)
  }
}

這兩個else if 什麼情況下成立呢?

  • 當 tagName 沒有在 stack 棧中找到對應的開始標簽時,pos 為 -1 。
  • tagName為br 、p標簽。

當你寫瞭 br 標簽的結束標簽:</br> 或 p 標簽的結束標簽 </p> 時,解析器能夠正常解析他們,其中對於 </br> 會將其解析為正常的 <br> 標簽,而 </p> 標簽也會正常解析為<p></p>。

可以發現對於 </br> 和 </p> 標簽瀏覽器可以將其正常解析為 <br> 以及<p></p>,Vue 的 parser 與瀏覽器的行為是一致的。

現在我們還剩一個問題沒有講解,即parseEndTag是如何處理stack棧中剩餘未處理的標簽的。其實就是調用 parseEndTag() 函數時不傳遞任何參數,也就是說此時 tagName 參數也不存在。這個時候我們再次查看下面的代碼:

由於 pos 為 0 ,所以 i >= pos 始終成立,這個時候 stack 棧中如果有剩餘未處理的標簽,則會逐個警告缺少閉合標簽,並調用 options.end 將其閉合。

以上對於整個詞法分析的過程重點部分就已經講解完畢瞭,其實現方式就是通過讀取字符流配合正則一點一點的解析字符串,直到整個字符串都被解析完畢為止。並且每當遇到一個特定的token 時都會調用相應的鉤子函數,同時將有用的參數傳遞過去。比如每當遇到一個開始標簽都會調用 options.start 鉤子函數,遇到閉合標簽調用 options.end 鉤子函數。

下面我們來講講這兩個重要的鉤子函數,並且談下AST的基本形成。

parseHTML 函數源碼解析 AST 基本形成

以上就是vue parseHTML函數解析器遇到結束標簽的詳細內容,更多關於vue parseHTML函數的資料請關註WalkonNet其它相關文章!

推薦閱讀: