您好,登錄后才能下訂單哦!
小編給大家分享一下Vue3中AST解析器的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
首先我們來重溫一下 baseCompile
函數中有關 ast 的邏輯及后續的使用:
export function baseCompile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { /* 忽略之前邏輯 */ const ast = isString(template) ? baseParse(template, options) : template transform( ast, {/* 忽略參數 */} ) return generate( ast, extend({}, options, { prefixIdentifiers }) ) }
因為我已經將咱們不需要關注的邏輯注釋處理,所以現在看函數體內的邏輯會非常清晰:
生成 ast 對象
將 ast
對象作為參數傳入 transform
函數,對 ast
節點進行轉換操作
將 ast 對象作為參數傳入 generate
函數,返回編譯結果
這里我們主要關注 ast 的生成。可以看到 ast 的生成有一個三目運算符的判斷,如果傳進來的 template
模板參數是一個字符串,那么則調用 baseParse
解析模板字符串,否則直接將 template
作為 ast
對象。baseParse
里做了什么事情才能生成 ast 呢?一起來看一下源碼,
export function baseParse( content: string, options: ParserOptions = {} ): RootNode { const context = createParserContext(content, options) // 創建解析的上下文對象 const start = getCursor(context) // 生成記錄解析過程的游標信息 return createRoot( // 生成并返回 root 根節點 parseChildren(context, TextModes.DATA, []), // 解析子節點,作為 root 根節點的 children 屬性 getSelection(context, start) ) }
在 baseParse
的函數中我添加了注釋,方便大家理解各個函數的作用,首先會創建解析的上下文,之后根據上下文獲取游標信息,由于還未進行解析,所以游標中的 column
、line
、offset
屬性對應的都是 template
的起始位置。之后就是創建根節點并返回根節點,至此ast 樹生成,解析完成。
export function createRoot( children: TemplateChildNode[], loc = locStub ): RootNode { return { type: NodeTypes.ROOT, children, helpers: [], components: [], directives: [], hoists: [], imports: [], cached: 0, temps: 0, codegenNode: undefined, loc } }
看 createRoot
函數的代碼,我們能發現該函數就是返回了一個 RootNode
類型的根節點對象,其中我們傳入的 children 參數會被作為根節點的 children
參數。這里非常好理解,按樹型數據結構來想象就可以。所以生成 ast 的關鍵點就會聚焦到 parseChildren
這個函數上來。parseChildren
函數如果不去看它的源碼,見文之意也可以大致了解這是一個解析子節點的函數。接下來我們就來一起來看一下 AST 解析中最關鍵的 parseChildren
函數,還是老規矩,為了幫助大家理解,我會精簡函數體內的邏輯。
function parseChildren( context: ParserContext, mode: TextModes, ancestors: ElementNode[] ): TemplateChildNode[] { const parent = last(ancestors) // 獲取當前節點的父節點 const ns = parent ? parent.ns : Namespaces.HTML const nodes: TemplateChildNode[] = [] // 存儲解析后的節點 // 當標簽未閉合時,解析對應節點 while (!isEnd(context, mode, ancestors)) {/* 忽略邏輯 */} // 處理空白字符,提高輸出效率 let removedWhitespace = false if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {/* 忽略邏輯 */} // 移除空白字符,返回解析后的節點數組 return removedWhitespace ? nodes.filter(Boolean) : nodes }
從上文代碼中,可以知道 parseChildren
函數接收三個參數,context
:解析器上下文,mode
:文本數據類型,ancestors
:祖先節點數組。而函數的執行中會首先從祖先節點中獲取當前節點的父節點,確定命名空間,以及創建一個空數組,用來儲存解析后的節點。之后會有一個 while 循環,判斷是否到達了標簽的關閉位置,如果不是需要關閉的標簽,則在循環體內對源模板字符串進行分類解析。之后會有一段處理空白字符的邏輯,處理完成后返回解析好的 nodes 數組。在大家對于 parseChildren
的執行流程有一個初步理解之后,我們一起來看一下函數的核心,while 循環內的邏輯。
在 while 中解析器會判斷文本數據的類型,只有當 TextModes
為 DATA 或 RCDATA 時會繼續往下解析。
第一種情況就是判斷是否需要解析 Vue 模板語法中的 “Mustache
”語法 (雙大括號) ,如果當前上下文中沒有 v-pre 指令來跳過表達式,并且源模板字符串是以我們指定的分隔符開頭的(此時 context.options.delimiters
中是雙大括號),就會進行雙大括號的解析。這里就可以發現,如果當你有特殊需求,不希望使用雙大括號作為表達式插值,那么你只需要在編譯前改變選項中的 delimiters
屬性即可。
接下來會判斷,如果第一個字符是 “<” 并且第二個字符是 '!'的話,會嘗試解析注釋標簽,<!DOCTYPE
和 <!CDATA
這三種情況,對于 DOCTYPE 會進行忽略,解析成注釋。
之后會判斷當第二個字符是 “/” 的情況,“</” 已經滿足了一個閉合標簽的條件了,所以會嘗試去匹配閉合標簽。當第三個字符是 “>”,缺少了標簽名字,會報錯,并讓解析器的進度前進三個字符,跳過 “</>”。
如果“</”開頭,并且第三個字符是小寫英文字符,解析器會解析結束標簽。
如果源模板字符串的第一個字符是 “<”,第二個字符是小寫英文字符開頭,會調用 parseElement
函數來解析對應的標簽。
當這個判斷字符串字符的分支條件結束,并且沒有解析出任何 node 節點,那么會將 node 作為文本類型,調用 parseText 進行解析。
最后將生成的節點添加進 nodes
數組,在函數結束時返回。
這就是 while 循環體內的邏輯,且是 parseChildren
中最重要的部分。在這個判斷過程中,我們看到了雙大括號語法的解析,看到了注釋節點的怎樣被解析的,也看到了開始標簽和閉合標簽的解析,以及文本內容的解析。精簡后的代碼在下方框中,大家可以對照上述的講解,來理解一下源碼。當然,源碼中的注釋也是非常詳細了喲。
while (!isEnd(context, mode, ancestors)) { const s = context.source let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined if (mode === TextModes.DATA || mode === TextModes.RCDATA) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) { /* 如果標簽沒有 v-pre 指令,源模板字符串以雙大括號 `{{` 開頭,按雙大括號語法解析 */ node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') { // 如果源模板字符串的第以個字符位置是 `!` if (s[1] === '!') { // 如果以 '<!--' 開頭,按注釋解析 if (startsWith(s, '<!--')) { node = parseComment(context) } else if (startsWith(s, '<!DOCTYPE')) { // 如果以 '<!DOCTYPE' 開頭,忽略 DOCTYPE,當做偽注釋解析 node = parseBogusComment(context) } else if (startsWith(s, '<![CDATA[')) { // 如果以 '<![CDATA[' 開頭,又在 HTML 環境中,解析 CDATA if (ns !== Namespaces.HTML) { node = parseCDATA(context, ancestors) } } // 如果源模板字符串的第二個字符位置是 '/' } else if (s[1] === '/') { // 如果源模板字符串的第三個字符位置是 '>',那么就是自閉合標簽,前進三個字符的掃描位置 if (s[2] === '>') { emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2) advanceBy(context, 3) continue // 如果第三個字符位置是英文字符,解析結束標簽 } else if (/[a-z]/i.test(s[2])) { parseTag(context, TagType.End, parent) continue } else { // 如果不是上述情況,則當做偽注釋解析 node = parseBogusComment(context) } // 如果標簽的第二個字符是小寫英文字符,則當做元素標簽解析 } else if (/[a-z]/i.test(s[1])) { node = parseElement(context, ancestors) // 如果第二個字符是 '?',當做偽注釋解析 } else if (s[1] === '?') { node = parseBogusComment(context) } else { // 都不是這些情況,則報出第一個字符不是合法標簽字符的錯誤。 emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1) } } } // 如果上述的情況解析完畢后,沒有創建對應的節點,則當做文本來解析 if (!node) { node = parseText(context, mode) } // 如果節點是數組,則遍歷添加進 nodes 數組中,否則直接添加 if (isArray(node)) { for (let i = 0; i < node.length; i++) { pushNode(nodes, node[i]) } } else { pushNode(nodes, node) } }
在 while
的循環內,各個分支判斷分支內,我們能看到 node
會接收各種節點類型的解析函數的返回值。而這里我會詳細的說一下 parseElement
這個解析元素的函數,因為這是我們在模板中用的最頻繁的場景。
我先把 parseElement
的源碼精簡一下貼上來,然后來嘮一嘮里面的邏輯。
function parseElement( context: ParserContext, ancestors: ElementNode[] ): ElementNode | undefined { // 解析起始標簽 const parent = last(ancestors) const element = parseTag(context, TagType.Start, parent) // 如果是自閉合的標簽或者是空標簽,則直接返回。voidTag例如: `<img>`, `<br>`, `<hr>` if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { return element } // 遞歸的解析子節點 ancestors.push(element) const mode = context.options.getTextMode(element, parent) const children = parseChildren(context, mode, ancestors) ancestors.pop() element.children = children // 解析結束標簽 if (startsWithEndTagOpen(context.source, element.tag)) { parseTag(context, TagType.End, parent) } else { emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start) if (context.source.length === 0 && element.tag.toLowerCase() === 'script') { const first = children[0] if (first && startsWith(first.loc.source, '<!--')) { emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT) } } } // 獲取標簽位置對象 element.loc = getSelection(context, element.loc.start) return element }
首先我們會獲取當前節點的父節點,然后調用 parseTag
函數解析。
parseTag 函數會按的執行大體是以下流程:
首先匹配標簽名。
解析元素中的 attribute 屬性,存儲至 props 屬性
檢測是否存在 v-pre 指令,若是存在的話,則修改 context 上下文中的 inVPre 屬性為 true
檢測自閉合標簽,如果是自閉合,則將 isSelfClosing 屬性置為 true
判斷 tagType,是 ELEMENT 元素還是 COMPONENT 組件,或者 SLOT 插槽
返回生成的 element 對象
在獲取到 element
對象后,會判斷 element
是否是自閉合標簽,或者是空標簽,例如 <img>, <br>, <hr> ,如果是這種情況,則直接返回 element
對象。
然后我們會嘗試解析 element
的子節點,將 element
壓入棧中中,然后遞歸的調用 parseChildren
來解析子節點。
const parent = last(ancestors)
再回頭看看 parseChildren
以及 parseElement
中的這行代碼,就可以發現在將 element
入棧后,我們拿到的父節點就是當前節點。在解析完畢后,調用 ancestors.pop()
,使當前解析完子節點的 element
對象出棧,將解析后的 children
對象賦值給 element
的 children
屬性,完成 element
的子節點解析,這里是個很巧妙的設計。
最后匹配結束標簽,設置 element 的 loc 位置信息,返回解析完畢的 element
對象。
請看下方我們要解析的模板,圖片中是解析過程中,保存解析后節點的棧的存儲情況,
<div> <p>Hello World</p> </div>
圖中的黃色矩形是一個棧,當開始解析時,parseChildren
首先會遇到 div 標簽,開始調用的 parseElement
函數。通過 parseTag 函數解析出了 div 元素,并將它壓入棧中,遞歸解析子節點。第二次調用 parseChildren 函數,遇見 p 元素,調用 parseElement 函數,將 p 標簽壓入棧中,此時棧中有 div 和 p 兩個標簽。再次解析 p 中的子節點,第三次調用 parseChildren
標簽,這次不會匹配到任何標簽,不會生成對應的 node,所以會通過 parseText 函數去生成文本,解析出 node 為 HelloWorld
,并返回 node。
將這個文本類型的 node
添加進 p 標簽的 children 屬性后,此時 p 標簽的子節點解析完畢,彈出祖先棧,完成結束標簽的解析后,返回 p 標簽對應的 element
對象。
p 標簽對應的 node 節點生成,并在 parseChildren
函數中返回對應 node。
div 標簽在接收到 p 標簽的 node 后,添加進自身的 children 屬性中,出棧。此時祖先棧中就空空如也了。而 div 的標簽完成閉合解析的邏輯后,返回 element
元素。
最終 parseChildren
的第一次調用返回結果,生成了 div 對應的 node 對象,也返回了結果,將這個結果作為 createRoot
函數的 children 參數傳入,生成根節點對象,完成 ast 解析。
以上是“Vue3中AST解析器的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。