小編給大家分享一下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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。