vue?parseHTML源碼解析hars?end?comment鉤子函數
引言
接上文 parseHTML 函數源碼解析 start鉤子函數
接下來我們主要講解當解析器遇到一個文本節(jié)點時會如何為文本節(jié)點創(chuàng)建元素描述對象,又會如何對文本節(jié)點做哪些特殊的處理。
parseHTML(template, { chars: function(){ //... }, //... })
chars源碼:
chars: function chars(text) { if (!currentParent) { { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.' ); } else if ((text = text.trim())) { warnOnce( ("text \"" + text + "\" outside root element will be ignored.") ); } } return } // IE textarea placeholder bug /* istanbul ignore if */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } var children = currentParent.children; text = inPre || text.trim() ? isTextTag(currentParent) ? text : decodeHTMLCached(text) // only preserve whitespace if its not right after a starting tag : preserveWhitespace && children.length ? ' ' : ''; if (text) { var res; if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text: text }); } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text: text }); } } }
當解析器遇到文本節(jié)點時,如上代碼中的 chars 鉤子函數就會被調用,并且接收該文本節(jié)點的文本內容作為參數。
我們來看chars鉤子函數最開始的這段代碼:
if (!currentParent) { { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.' ); } else if ((text = text.trim())) { warnOnce( ("text \"" + text + "\" outside root element will be ignored.") ); } } return }
首先判斷了 currentParent 變量是否存在,我們知道 currentParent 變量指向的是當前節(jié)點的父節(jié)點:。
如果 currentParent 變量不存在說明什么問題?
- 1:沒有根元素,只有文本。
- 2: 文本在根元素之外。
當遇到第一種情況打印警告信息:"模板必須要有根元素",第二種情況打印警告信息:" 根元素外的文本將會被忽略"。
接下來:
if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return }
這段代碼是用來解決 IE 瀏覽器中渲染 <textarea> 標簽的 placeholder 屬性時存在的 bug 的。具體的問題大家可以在這個 issue 查看。
接下來是個嵌套三元表達式:
var children = currentParent.children; text = inPre || text.trim() ? isTextTag(currentParent) ? text : decodeHTMLCached(text) // only preserve whitespace if its not right after a starting tag : preserveWhitespace && children.length ? ' ' : '';
這個嵌套三元表達式判斷了條件 inPre || text.trim() 的真假,如果為 true,檢測了當前文本節(jié)點的父節(jié)點是否是文本標簽,如果是文本標簽則直接使用原始文本,否則使用decodeHTMLCached 函數對文本進行解碼。
inPre || text.trim() 如果為 false,檢測 preserveWhitespace 是否為 true 。preserveWhitespace 是一個布爾值代表著是否保留空格,只有它為真的情況下才會保留空格。但即使 preserveWhitespace 常量的值為真,如果當前節(jié)點的父節(jié)點沒有子元素則也不會保留空格,換句話說,編譯器只會保留那些 不存在于開始標簽之后的空格。
接下來:
if (text) { var res; if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text: text }); } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text: text }); } }
這里相當就比較簡單了一個 if else if 操作,第一個 if 判斷當前元素未使用v-pre 指令,text不為空,使用 parseText 函數成功解析當前文本節(jié)點的內容。
對于前兩個條件很好理解,關鍵在于 parseText 函數能夠成功解析文本節(jié)點的內容說明了什么,如下示例代碼:
<div> hello: {{ message }} </div>
如上模板中存在的文本節(jié)點包含了 Vue 語法中的字面量表達式,而 parseText 函數的作用就是用來解析這段包含了字面量表達式的文本的。此時會執(zhí)行以下代碼創(chuàng)建一個類型為2(type = 2) 的元素描述對象:
children.push({ type: 2, expression: res.expression, tokens: res.tokens, text: text });
注意:類型為 2 的元素描述對象擁有三個特殊的屬性,分別是 expression 、tokens 以及text ,其中 text 就是原始的文本內容,而 expression 和 tokens 的值是通過 parseText 函數解析的結果中讀取的。
后面我們專門會講講parseText函數,接下來繼續(xù)看下如果上列的 if 判斷失敗出現的三種可能性。
- 當前解析的元素使用v-pre 指令
- text 為空
- parseText 解析失敗
只要以上三種情況中,有一種情況出現則代碼會來到else...if 分支的判斷,如下:
else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { children.push({ type: 3, text: text }); }
如果滿足 else if 中的條件直接,創(chuàng)建一個類型為3(type = 3) 的元素描述對象:類型為3 的元素描述對象只擁有一個的屬性text存儲原始的文本內容。
在看下要滿足 else if 中的這些條件吧!
- 文本內容不是空格
- 文本內容是空格,但是該文本節(jié)點的父節(jié)點還沒有子節(jié)點(即 !children.length )
- 文本內容是空格,并且該文本節(jié)點的父節(jié)點有子節(jié)點,但最后一個子節(jié)點不是空格
接下來我們來聊聊之前講到的parseText 函數。
parseText
function parseText( text, delimiters ) { var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE; if (!tagRE.test(text)) { return } var tokens = []; var rawTokens = []; var lastIndex = tagRE.lastIndex = 0; var match, index, tokenValue; while ((match = tagRE.exec(text))) { index = match.index; // push text token if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)); tokens.push(JSON.stringify(tokenValue)); } // tag token var exp = parseFilters(match[1].trim()); tokens.push(("_s(" + exp + ")")); rawTokens.push({ '@binding': exp }); lastIndex = index + match[0].length; } if (lastIndex < text.length) { rawTokens.push(tokenValue = text.slice(lastIndex)); tokens.push(JSON.stringify(tokenValue)); } return { expression: tokens.join('+'), tokens: rawTokens } }
parseText 接收兩個參數 text 要解析的文本,delimiters 是編譯器的一個用戶自定義選項delimiters ,通過它可以改變文本插入分隔符。所以才有了如下代碼。
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
這里是解析文本所用正則之間的一個較量,delimiters 有值就調用buildRegex函數,我們默認是沒有值,使用 defaultTagRE 來解析文本。
var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
這個正則還是非常簡單,接下來會判斷,如果文本中沒有與正則相匹配的文本直接直接終止函數的執(zhí)行。
if (!tagRE.test(text)) { return }
接下來代碼就有意思了一起看下。
var tokens = []; var rawTokens = []; var lastIndex = tagRE.lastIndex = 0; var match, index, tokenValue; while ((match = tagRE.exec(text))) { index = match.index; // push text token if (index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)); tokens.push(JSON.stringify(tokenValue)); } // tag token var exp = parseFilters(match[1].trim()); tokens.push(("_s(" + exp + ")")); rawTokens.push({ '@binding': exp }); lastIndex = index + match[0].length; } if (lastIndex < text.length) { rawTokens.push(tokenValue = text.slice(lastIndex)); tokens.push(JSON.stringify(tokenValue)); } return { expression: tokens.join('+'), tokens: rawTokens }
這段代碼不難,初始定義了一系列變量。 接著開啟一個while循環(huán),使用 tagRE 正則匹配文本內容,并將匹配結果保存在 match 變量中,直到匹配失敗循環(huán)才會終止,這時意味著所有的字面量表達式都已經處理完畢了。
在這個while循環(huán)結束返回一個對象,expression、tokens分別存儲解析過程中的信息。
假設文本如下:
<div id="app">hello {{ message }}</div>
parseText 解析文本后返回的對象。
{ expression: "'hello'+_s(message)", tokens: [ 'hello', { '@binding': 'message' } ] }
接下來我們聊聊對結束標簽的處理。
end 源碼
end: function end() { // remove trailing whitespace var element = stack[stack.length - 1]; var lastNode = element.children[element.children.length - 1]; if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop(); } // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; closeElement(element); }
end 鉤子函數,當解析 html 字符串遇到結束標簽的時候,。那么在 end 鉤子函數中都需要做哪些事情呢?
在之前的文章中我們講過解析器遇到非一元標簽的開始標簽時,會將該標簽的元素描述對象設置給 currentParent 變量,代表后續(xù)解析過程中遇到的所有標簽都應該是 currentParent 變量所代表的標簽的子節(jié)點,同時還會將該標簽的元素描述對象添加到 stack 棧中。
而當遇到結束標簽的時候則意味著 currentParent 變量所代表的標簽以及其子節(jié)點全部解析完畢了,此時我們應該把 currentParent 變量的引用修改為當前標簽的父標簽,這樣我們就將作用域還原給了上層節(jié)點,以保證解析過程中正確的父子關系。
下面代碼就是來完成這個工作:
stack.length -= 1; currentParent = stack[stack.length - 1]; closeElement(element);
首先將當前節(jié)點出棧:stack.length -= 1 什么意思呢?
看一個代碼就懂了。
var arr = [1,2,3,4]; arr.length-=1; >arr [1,2,3]
接著讀取出棧后 stack 棧中的最后一個元素作為 currentParent 變量的值。 那closeElement 函數是做什么用的呢?
closeElement 源碼
function closeElement(element) { // check pre state if (element.pre) { inVPre = false; } if (platformIsPreTag(element.tag)) { inPre = false; } // apply post-transforms for (var i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options); } }
closeElement 的作用有兩個:第一個是對數據狀態(tài)的還原,第二個是調用后置處理轉換鉤子函數。
接下來看下end函數中剩余代碼:
// remove trailing whitespace var element = stack[stack.length - 1]; var lastNode = element.children[element.children.length - 1]; if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop(); }
這個代碼的作用是去除當前元素最后一個空白子節(jié)點,我們在講解 chars 鉤子函數時了解到:preserveWhitespace 只會保留那些不在開始標簽之后的空格,所以當空白作為標簽的最后一個子節(jié)點存在時,也會被保留,如下代碼所示:
<div><span>test</span> <!-- 空白占位 --> </div>
如上代碼中 <span> 標簽的結束標簽與 <div> 標簽的結束標簽之間存在一段空白,這段空白將會被保留。如果這段空白被保留那么就可能對布局產生影響,尤其是對行內元素的影響。
為了消除這些影響帶來的問題,好的做法是將它們去掉,而如代碼就是用來完成這個工作的。
comment 注釋節(jié)點描述對象
解析器是否會解析并保留注釋節(jié)點,是由 shouldKeepComment 編譯器選項決定的,開發(fā)者可以在創(chuàng)建Vue 實例的時候通過設置 comments 選項的值來控制編譯器的shouldKeepComment 選項。默認情況下 comments 選項的值為 false ,即不保留注釋,假如將其設置為 true ,則當解析器遇到注釋節(jié)點時會保留該注釋節(jié)點,此時 parseHTML 函數的 comment 鉤子函數會被調用,如下:
comment: function comment(text) { currentParent.children.push({ type: 3, text: text, isComment: true }); }
要注意的是,普通文本節(jié)點與注釋節(jié)點的元素描述對象的類型是一樣的都是 3 ,不同的是注釋節(jié)點的元素描述對象擁有 isComment 屬性,并且該屬性的值為 true,目的就是用來與普通文本節(jié)點作區(qū)分的。
以上就是vue parseHTML源碼解析hars end comment鉤子函數的詳細內容,更多關于vue parseHTML鉤子函數的資料請關注腳本之家其它相關文章!
相關文章
利用Vue3 (一)創(chuàng)建Vue CLI 項目
這篇文章主要介紹利用Vue3 創(chuàng)建Vue CLI 項目,下面文章內容附有官方文檔鏈接,安裝過程,需要的可以參考一下2021-10-10Vue使用ElementUI動態(tài)修改table單元格背景顏色或文本顏色
本文主要介紹了Vue使用ElementUI動態(tài)修改table單元格背景顏色或文本顏色,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-02-02