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