接上篇:現(xiàn)在看看,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; // 開啟一個(gè) while 循環(huán),循環(huán)結(jié)束的條件是 html 為空,即 html 被 parse 完畢 while (html) { last = html; if (!lastTag || !isPlainTextElement(lastTag)) { // 確保即將 parse 的內(nèi)容不是在純文本標(biāo)簽里 (script,style,textarea) } else { // parse 的內(nèi)容是在純文本標(biāo)簽里 (script,style,textarea) } //將整個(gè)字符串作為文本對(duì)待 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 開始標(biāo)簽 function parseStartTag() { //... } //處理 parseStartTag 的結(jié)果 function handleStartTag(match) { //... } //parse 結(jié)束標(biāo)簽 function parseEndTag(tagName, start, end) { //... } }
可以看到 parseHTML 函數(shù)接收兩個(gè)參數(shù):html 和 options ,其中 html 是要被編譯的字符串,而options則是編譯器所需的選項(xiàng)。
整體上來講 parseHTML分為三部分。
函數(shù)開頭定義的一些常量和變量
while 循環(huán)
parse 過程中需要用到的 analytic function
函數(shù)開頭定義的一些常量和變量
先從第一部分開始講起
var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; var index = 0; var last, lastTag;
第一個(gè)變量是 stack,它被初始化為一個(gè)空數(shù)組,在 while 循環(huán)中處理 html 字符流的時(shí)候每當(dāng)遇到一個(gè)非單標(biāo)簽,都會(huì)將該開始標(biāo)簽 push 到該數(shù)組。它的作用模板中 DOM 結(jié)構(gòu)規(guī)范性的檢測。
但在一個(gè) html 字符串中,如何判斷一個(gè)非單標(biāo)簽是否缺少結(jié)束標(biāo)簽?zāi)兀?/p>
假設(shè)我們有如下html字符串:
<div><p><span></p></div>
在編譯這個(gè)字符串的時(shí)候,首先會(huì)遇到 div 開始標(biāo)簽,并將該 push 到 stack 數(shù)組,然后會(huì)遇到 p 開始標(biāo)簽,并將該標(biāo)簽 push 到 stack ,接下來會(huì)遇到 span 開始標(biāo)簽,同樣被 push 到 stack ,此時(shí) stack 數(shù)組內(nèi)包含三個(gè)元素。
再然后便會(huì)遇到 p 結(jié)束標(biāo)簽,按照正常邏輯可以推理出最先遇到的結(jié)束標(biāo)簽,其對(duì)應(yīng)的開始標(biāo)簽應(yīng)該最后被push到 stack 中,也就是說 stack 棧頂?shù)脑貞?yīng)該是 span ,如果不是 span 而是 p,這說明 span 元素缺少閉合標(biāo)簽。
這就是檢測 html 字符串中是否缺少閉合標(biāo)簽的原理。
第二個(gè)變量是 expectHTML,它的值被初始化為 options.expectHTML,也就是編譯器選項(xiàng)中的 expectHTML。
第三個(gè)常量是 isUnaryTag,用來檢測一個(gè)標(biāo)簽是否是一元標(biāo)簽。
第四個(gè)常量是 canBeLeftOpenTag,用來檢測一個(gè)標(biāo)簽是否是可以省略閉合標(biāo)簽的非一元標(biāo)簽。
index 初始化為 0 ,標(biāo)識(shí)著當(dāng)前字符流的讀入位置。
last 存儲(chǔ)剩余還未編譯的 html 字符串。
lastTag 始終存儲(chǔ)著位于 stack 棧頂?shù)脑亍?/p>
while 循環(huán)
接下來將進(jìn)入第二部分,即開啟一個(gè) while 循環(huán),循環(huán)的終止條件是 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 } }
首先將在每次循環(huán)開始時(shí)將 html 的值賦給變量 last :
last = html;
為什么這么做?在 while 循環(huán)即將結(jié)束的時(shí)候,有一個(gè)對(duì) last 和 html 這兩個(gè)變量的比較,在此可以找到答案:
if (html === last) {}
如果兩者相等,則說明html 在經(jīng)歷循環(huán)體的代碼之后沒有任何改變,此時(shí)會(huì)"Mal-formatted tag at end of template: \"" + html + "\"" 錯(cuò)誤信息提示。
接下來可以簡單看下整體while循環(huán)的結(jié)構(gòu)。
while (html) { last = html if (!lastTag || !isPlainTextElement(lastTag)) { // parse 的內(nèi)容不是在純文本標(biāo)簽里 } else { // parse 的內(nèi)容是在純文本標(biāo)簽里 (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 } }
接下來我們重點(diǎn)來分析這個(gè)if else 中的代碼。
!lastTag || !isPlainTextElement(lastTag)
lastTag 剛剛講到它會(huì)一直存儲(chǔ) stack 棧頂?shù)脑兀钱?dāng)編譯器剛開始工作時(shí),他只是一個(gè)空數(shù)組對(duì)象,![] == false
isPlainTextElement(lastTag) 檢測 lastTag 是否為純標(biāo)簽內(nèi)容。
var isPlainTextElement = makeMap('script,style,textarea', true);
lastTag 為空數(shù)組 ,isPlainTextElement(lastTag ) 返回false, !isPlainTextElement(lastTag) ==true, 有興趣的同學(xué)可以閱讀下 makeMap 源碼。
接下來我們繼續(xù)往下看,簡化版的代碼。
if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<') if (textEnd === 0) { // 第一個(gè)字符就是(<)尖括號(hào) } var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { //第一個(gè)字符不是(<)尖括號(hào) } if (textEnd < 0) { // 第一個(gè)字符不是(<)尖括號(hào) } if (options.chars && text) { options.chars(text) } } else { // 省略 ... }
textEnd ===0
當(dāng) textEnd === 0 時(shí),說明 html 字符串的第一個(gè)字符就是左尖括號(hào),比如 html 字符串為:<div>box</div>,那么這個(gè)字符串的第一個(gè)字符就是左尖括號(hào)(<)。
if (textEnd === 0) { // Comment: 如果是注釋節(jié)點(diǎn) if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd)); } advance(commentEnd + 3); continue } } //如果是條件注釋節(jié)點(diǎn) if (conditionalComment.test(html)) { var conditionalEnd = html.indexOf(']>'); if (conditionalEnd >= 0) { advance(conditionalEnd + 2); continue } } // 如果是 Doctyp節(jié)點(diǎn) var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue } // End tag: 結(jié)束標(biāo)簽 var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue } // Start tag: 開始標(biāo)簽 var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } }
細(xì)枝末節(jié)我們不看,重點(diǎn)在End tag 、 Start tag 上。
我們先從解析標(biāo)簽開始分析
var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue }
parseStartTag 函數(shù)解析開始標(biāo)簽
解析開始標(biāo)簽會(huì)調(diào)用parseStartTag函數(shù),如果有返回值,說明開始標(biāo)簽解析成功。
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 函數(shù)首先會(huì)調(diào)用 html 字符串的 match 函數(shù)匹配 startTagOpen 正則,前面我們分析過編譯器所需的正則。
Vue編譯器token解析規(guī)則-正則分析
如果匹配成功,那么start 將是一個(gè)包含兩個(gè)元素的數(shù)組:第一個(gè)元素是標(biāo)簽的開始部分(包含< 和 標(biāo)簽名稱);第二個(gè)元素是捕獲組捕獲到的標(biāo)簽名稱。比如有如下template:
<div></div>
start為:
start = ['<div', 'div']
接下來:
定義了 match 變量,它是一個(gè)對(duì)象,初始狀態(tài)下?lián)碛腥齻€(gè)屬性:
tagName:它的值為 start[1] 即標(biāo)簽的名稱。
attrs :這個(gè)數(shù)組就是用來存儲(chǔ)將來被匹配到的屬性。
start:初始值為 index,是當(dāng)前字符流讀入位置在整個(gè) html 字符串中的相對(duì)位置。
advance(start[0].length);
相對(duì)就比較簡單了,他的作用就是在源字符中截取已經(jīng)編譯完成的字符,我們知道當(dāng)html 字符為 “”,整個(gè)詞法分析的工作就結(jié)束了,在這中間扮演重要角色的就是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循環(huán),循環(huán)的條件有兩個(gè),第一個(gè)條件是:沒有匹配到開始標(biāo)簽的結(jié)束部分,這個(gè)條件的實(shí)現(xiàn)方式主要使用了 startTagClose 正則,并將結(jié)果保存到 end 變量中。
第二個(gè)條件是:匹配到了屬性,主要使用了attribute正則。
總結(jié)下這個(gè)while循環(huán)成立要素:沒有匹配到開始標(biāo)簽的結(jié)束部分,并且匹配到了開始標(biāo)簽中的屬性,這個(gè)時(shí)候循環(huán)體將被執(zhí)行,直到遇到開始標(biāo)簽的結(jié)束部分為止。
接下來在循環(huán)體內(nèi)做了兩件事,首先調(diào)用advance函數(shù),參數(shù)為attr[0].length即整個(gè)屬性的長度。然后會(huì)將此次循環(huán)匹配到的結(jié)果push到前面定義的match對(duì)象的attrs數(shù)組中。
advance(attr[0].length); match.attrs.push(attr);
接下來看下最后這部分代碼。
if (end) { match.unarySlash = end[1]; advance(end[0].length); match.end = index; return match }
首先判斷了變量 end 是否為真,我們知道,即使匹配到了開始標(biāo)簽的開始部分以及屬性部分但是卻沒有匹配到開始標(biāo)簽的結(jié)束部分,這說明這根本就不是一個(gè)開始標(biāo)簽。所以只有當(dāng)變量end存在,即匹配到了開始標(biāo)簽的結(jié)束部分時(shí),才能說明這是一個(gè)完整的開始標(biāo)簽。
如果變量end的確存在,那么將會(huì)執(zhí)行 if 語句塊內(nèi)的代碼,不過我們需要先了解一下變量end的值是什么?
比如當(dāng)html(template)字符串如下時(shí):
<br />
那么匹配到的end的值為:
end = ['/>', '/']
比如當(dāng)html(template)字符串如下時(shí):
<div></div>
那么匹配到的end的值為:
end = ['>', undefined]
結(jié)論如果end[1]不為undefined,那么說明該標(biāo)簽是一個(gè)一元標(biāo)簽。
那么現(xiàn)在再看if語句塊內(nèi)的代碼,將很容易理解,首先在match對(duì)象上添加unarySlash屬性,其值為end[1]
match.unarySlash = end[1];
然后調(diào)用advance函數(shù),參數(shù)為end[0].length,接著在match 對(duì)象上添加了一個(gè)end屬性,它的值為index,注意由于先調(diào)用的advance函數(shù),所以此時(shí)的index已經(jīng)被更新了。最后將match 對(duì)象作為 parseStartTag 函數(shù)的返回值返回。
只有當(dāng)變量end存在時(shí),即能夠確定確實(shí)解析到了一個(gè)開始標(biāo)簽的時(shí)候parseStartTag函數(shù)才會(huì)有返回值,并且返回值是match對(duì)象,其他情況下parseStartTag全部返回undefined。
總結(jié):
我們模擬假設(shè)有如下html(template)字符串:
<div id="box" v-if="watings"></div>
則parseStartTag函數(shù)的返回值如下:
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函數(shù)及其返回值,現(xiàn)在我們回到對(duì)開始標(biāo)簽的 parse 部分,接下來我們會(huì)繼續(xù)講解,拿到返回值之后的處理。
var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue }
篇幅有限請移步:
parseHTML 函數(shù)源碼解析返回值后的處理
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/127764.html
摘要:當(dāng)前正在處理的節(jié)點(diǎn),以及該節(jié)點(diǎn)的和等信息。源碼解析之一整體分析源碼解析之三寫作中源碼解析之四寫作中作者博客作者作者微博 筆者系 vue-loader 貢獻(xiàn)者之一(#16) 前言 vue-loader 源碼解析系列之一,閱讀該文章之前,請大家首先參考大綱 vue-loader 源碼解析系列之 整體分析 selector 做了什么 const path = require(path) co...
寫文章不容易,點(diǎn)個(gè)贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點(diǎn)擊 下面鏈接 或者 拉到 下面關(guān)注公眾號(hào)也可以吧 【Vue原理】Compile - 源碼版 之 Parse 主要流程 本文難度較繁瑣,需要耐心觀看,如果你對(duì) compile 源碼暫時(shí)...
摘要:下面用具體代碼進(jìn)行分析。匹配不到那么就是開始標(biāo)簽,調(diào)用函數(shù)解析。如這里的轉(zhuǎn)化為加上是為了的下一步轉(zhuǎn)為函數(shù),本文中暫時(shí)不會(huì)用到。再把轉(zhuǎn)化后的內(nèi)容進(jìn)。 什么是AST 在Vue的mount過程中,template會(huì)被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式。...
摘要:當(dāng)字符串開頭是時(shí),可以匹配匹配尾標(biāo)簽。從結(jié)尾,找到所在位置批量閉合。 寫文章不容易,點(diǎn)個(gè)贊唄兄弟 專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點(diǎn)擊 下面鏈接 或者 拉到 下面關(guān)注公眾號(hào)也可以吧 【Vue原理】Compile - 源碼版 之 標(biāo)簽解析...
vue parseHTML函數(shù)解析器遇到結(jié)束標(biāo)簽,在之前文章中已講述完畢。 例如有html(template)字符串: <divid="app"> <p>{{message}}</p> </div> 產(chǎn)出如下: { attrs:["id="app"","id...
在說Vue parse源碼之前,首先要了解周邊的工具函數(shù)。 之前見過element元素節(jié)點(diǎn)四描述對(duì)象? varelement={ type:1, tag:tag, parent:null, attrsList:attrs, children:[] } 是用一個(gè)createASTElement函數(shù),創(chuàng)建函數(shù)對(duì)象。 createASTElement函數(shù) funct...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28