您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“vue parseHTML函數源碼分析”,內容詳細,步驟清晰,細節處理妥當,希望這篇“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 循環,循環的終止條件是 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 時,說明 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函數,如果有返回值,說明開始標簽解析成功。
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:
<div></div>
start為:
start = ['<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)) && (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 }
讀到這里,這篇“vue parseHTML函數源碼分析”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。