<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
接上篇:
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分為三部分。
先從第一部分開始講起
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,用來檢測一個標籤是否是可以省略閉合標籤的非一元標籤。
接下來將進入第二部分,即開啟一個 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 時,說明 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 變數,它是一個物件,初始狀態下擁有三個屬性:
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 函數原始碼解析的詳細內容,更多關於vue parseHTML函數的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45