首頁 > 軟體

vue parseHTML 函數原始碼解析

2022-07-13 18:01:10

正文

接上篇:

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(/<!--([sS]*?)-->/g, '$1') // #7298
					.replace(/<![CDATA[([sS]*?)]]>/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函數的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com