欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3 AST解析器-源碼解析

 更新時間:2021年09月25日 11:26:57   作者:Originalee  
這篇文章我們從 ast 生成時調(diào)用的 baseParse 函數(shù)分析,再到 baseParse 返回 createRoot 的調(diào)用結(jié)果,一直到細化的講解了 parseChildren 解析子節(jié)點函數(shù)中的其中某一個具體解析器的執(zhí)行過程。最后通過一個簡單模板舉例,需要的朋友可以參考下

上一篇文章Vue3 編譯流程-源碼解析中,我們從 packges/vue/src/index.ts 的入口開始,了解了一個 Vue 對象的編譯流程,在文中我們提到 baseCompile 函數(shù)在執(zhí)行過程中會生成 AST 抽象語法樹,毫無疑問這是很關(guān)鍵的一步,因為只有拿到生成的 AST 我們才能遍歷 AST 的節(jié)點進行 transform 轉(zhuǎn)換操作,比如解析 v-ifv-for 等各種指令,或者對節(jié)點進行分析將滿足條件的節(jié)點靜態(tài)提升,這些都依賴之前生成的 AST 抽象語法樹。那么今天我們就一起來看一下 AST 的解析,看看 Vue 是如何解析模板的。

1、生成 AST 抽象語法樹

首先我們來重溫一下 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
    })
  )
}

因為我已經(jīng)將咱們不需要關(guān)注的邏輯注釋處理,所以現(xiàn)在看函數(shù)體內(nèi)的邏輯會非常清晰:

  • 生成 ast 對象
  • ast 對象作為參數(shù)傳入 transform 函數(shù),對 ast 節(jié)點進行轉(zhuǎn)換操作
  • 將 ast 對象作為參數(shù)傳入 generate 函數(shù),返回編譯結(jié)果

這里我們主要關(guān)注 ast 的生成??梢钥吹?ast 的生成有一個三目運算符的判斷,如果傳進來的 template 模板參數(shù)是一個字符串,那么則調(diào)用 baseParse 解析模板字符串,否則直接將 template 作為 ast 對象。baseParse 里做了什么事情才能生成 ast 呢?一起來看一下源碼,

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(content, options) // 創(chuàng)建解析的上下文對象
  const start = getCursor(context) // 生成記錄解析過程的游標信息
  return createRoot( // 生成并返回 root 根節(jié)點
    parseChildren(context, TextModes.DATA, []), // 解析子節(jié)點,作為 root 根節(jié)點的 children 屬性
    getSelection(context, start)
  )
}

baseParse 的函數(shù)中我添加了注釋,方便大家理解各個函數(shù)的作用,首先會創(chuàng)建解析的上下文,之后根據(jù)上下文獲取游標信息,由于還未進行解析,所以游標中的 columnline、offset 屬性對應的都是 template 的起始位置。之后就是創(chuàng)建根節(jié)點并返回根節(jié)點,至此ast 樹生成,解析完成。

2、創(chuàng)建 AST 的根節(jié)點

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ù)就是返回了一個 RootNode 類型的根節(jié)點對象,其中我們傳入的 children 參數(shù)會被作為根節(jié)點的 children 參數(shù)。這里非常好理解,按樹型數(shù)據(jù)結(jié)構(gòu)來想象就可以。所以生成 ast 的關(guān)鍵點就會聚焦到 parseChildren 這個函數(shù)上來。parseChildren 函數(shù)如果不去看它的源碼,見文之意也可以大致了解這是一個解析子節(jié)點的函數(shù)。接下來我們就來一起來看一下 AST 解析中最關(guān)鍵的 parseChildren 函數(shù),還是老規(guī)矩,為了幫助大家理解,我會精簡函數(shù)體內(nèi)的邏輯。

3、解析子節(jié)點

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors) // 獲取當前節(jié)點的父節(jié)點
  const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = [] // 存儲解析后的節(jié)點

  // 當標簽未閉合時,解析對應節(jié)點
  while (!isEnd(context, mode, ancestors)) {/* 忽略邏輯 */}

  // 處理空白字符,提高輸出效率
  let removedWhitespace = false
  if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {/* 忽略邏輯 */}

  // 移除空白字符,返回解析后的節(jié)點數(shù)組
  return removedWhitespace ? nodes.filter(Boolean) : nodes
}

從上文代碼中,可以知道 parseChildren 函數(shù)接收三個參數(shù),context:解析器上下文,mode:文本數(shù)據(jù)類型,ancestors:祖先節(jié)點數(shù)組。而函數(shù)的執(zhí)行中會首先從祖先節(jié)點中獲取當前節(jié)點的父節(jié)點,確定命名空間,以及創(chuàng)建一個空數(shù)組,用來儲存解析后的節(jié)點。之后會有一個 while 循環(huán),判斷是否到達了標簽的關(guān)閉位置,如果不是需要關(guān)閉的標簽,則在循環(huán)體內(nèi)對源模板字符串進行分類解析。之后會有一段處理空白字符的邏輯,處理完成后返回解析好的 nodes 數(shù)組。在大家對于 parseChildren 的執(zhí)行流程有一個初步理解之后,我們一起來看一下函數(shù)的核心,while 循環(huán)內(nèi)的邏輯。

在 while 中解析器會判斷文本數(shù)據(jù)的類型,只有當 TextModes 為 DATA 或 RCDATA 時會繼續(xù)往下解析。

第一種情況就是判斷是否需要解析 Vue 模板語法中的 “Mustache”語法 (雙大括號) ,如果當前上下文中沒有 v-pre 指令來跳過表達式,并且源模板字符串是以我們指定的分隔符開頭的(此時 context.options.delimiters 中是雙大括號),就會進行雙大括號的解析。這里就可以發(fā)現(xiàn),如果當你有特殊需求,不希望使用雙大括號作為表達式插值,那么你只需要在編譯前改變選項中的 delimiters 屬性即可。

接下來會判斷,如果第一個字符是 “<” 并且第二個字符是 '!'的話,會嘗試解析注釋標簽,<!DOCTYPE <!CDATA 這三種情況,對于 DOCTYPE 會進行忽略,解析成注釋。

之后會判斷當?shù)诙€字符是 “/” 的情況,“</” 已經(jīng)滿足了一個閉合標簽的條件了,所以會嘗試去匹配閉合標簽。當?shù)谌齻€字符是 “>”,缺少了標簽名字,會報錯,并讓解析器的進度前進三個字符,跳過 “</>”。

如果“</”開頭,并且第三個字符是小寫英文字符,解析器會解析結(jié)束標簽。

如果源模板字符串的第一個字符是 “<”,第二個字符是小寫英文字符開頭,會調(diào)用 parseElement 函數(shù)來解析對應的標簽。

當這個判斷字符串字符的分支條件結(jié)束,并且沒有解析出任何 node 節(jié)點,那么會將 node 作為文本類型,調(diào)用 parseText 進行解析。

最后將生成的節(jié)點添加進 nodes 數(shù)組,在函數(shù)結(jié)束時返回。

這就是 while 循環(huán)體內(nèi)的邏輯,且是 parseChildren 中最重要的部分。在這個判斷過程中,我們看到了雙大括號語法的解析,看到了注釋節(jié)點的怎樣被解析的,也看到了開始標簽和閉合標簽的解析,以及文本內(nèi)容的解析。精簡后的代碼在下方框中,大家可以對照上述的講解,來理解一下源碼。當然,源碼中的注釋也是非常詳細了喲。

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 環(huán)境中,解析 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
        // 如果第三個字符位置是英文字符,解析結(jié)束標簽
        } 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)
      }
    }
  }
  
  // 如果上述的情況解析完畢后,沒有創(chuàng)建對應的節(jié)點,則當做文本來解析
  if (!node) {
    node = parseText(context, mode)
  }
  
  // 如果節(jié)點是數(shù)組,則遍歷添加進 nodes 數(shù)組中,否則直接添加
  if (isArray(node)) {
    for (let i = 0; i < node.length; i++) {
      pushNode(nodes, node[i])
    }
  } else {
    pushNode(nodes, node)
  }
}

4、解析模板元素 Element

while 的循環(huán)內(nèi),各個分支判斷分支內(nèi),我們能看到 node 會接收各種節(jié)點類型的解析函數(shù)的返回值。而這里我會詳細的說一下 parseElement 這個解析元素的函數(shù),因為這是我們在模板中用的最頻繁的場景。

我先把 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
  }

  // 遞歸的解析子節(jié)點
  ancestors.push(element)
  const mode = context.options.getTextMode(element, parent)
  const children = parseChildren(context, mode, ancestors)
  ancestors.pop()

  element.children = children

  // 解析結(jié)束標簽
  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
}

首先我們會獲取當前節(jié)點的父節(jié)點,然后調(diào)用 parseTag 函數(shù)解析。

parseTag 函數(shù)會按的執(zhí)行大體是以下流程:

  • 首先匹配標簽名。
  • 解析元素中的 attribute 屬性,存儲至 props 屬性
  • 檢測是否存在 v-pre 指令,若是存在的話,則修改 context 上下文中的 inVPre 屬性為 true
  • 檢測自閉合標簽,如果是自閉合,則將 isSelfClosing 屬性置為 true
  • 判斷 tagType,是 ELEMENT 元素還是 COMPONENT 組件,或者 SLOT 插槽
  • 返回生成的 element 對象

在獲取到 element 對象后,會判斷 element 是否是自閉合標簽,或者是空標簽,例如 <img>, <br>, <hr> ,如果是這種情況,則直接返回 element 對象。

然后我們會嘗試解析 element 的子節(jié)點,將 element 壓入棧中中,然后遞歸的調(diào)用 parseChildren 來解析子節(jié)點。

const parent = last(ancestors)

再回頭看看 parseChildren 以及 parseElement 中的這行代碼,就可以發(fā)現(xiàn)在將 element 入棧后,我們拿到的父節(jié)點就是當前節(jié)點。在解析完畢后,調(diào)用 ancestors.pop() ,使當前解析完子節(jié)點的 element 對象出棧,將解析后的 children 對象賦值給 element children 屬性,完成 element 的子節(jié)點解析,這里是個很巧妙的設計。

最后匹配結(jié)束標簽,設置 element 的 loc 位置信息,返回解析完畢的 element 對象。

5、示例:模板元素解析

請看下方我們要解析的模板,圖片中是解析過程中,保存解析后節(jié)點的棧的存儲情況,

<div>
  <p>Hello World</p>
</div>

圖中的黃色矩形是一個棧,當開始解析時,parseChildren 首先會遇到 div 標簽,開始調(diào)用的 parseElement 函數(shù)。通過 parseTag 函數(shù)解析出了 div 元素,并將它壓入棧中,遞歸解析子節(jié)點。第二次調(diào)用 parseChildren 函數(shù),遇見 p 元素,調(diào)用 parseElement 函數(shù),將 p 標簽壓入棧中,此時棧中有 div 和 p 兩個標簽。再次解析 p 中的子節(jié)點,第三次調(diào)用 parseChildren 標簽,這次不會匹配到任何標簽,不會生成對應的 node,所以會通過 parseText 函數(shù)去生成文本,解析出 node 為 HelloWorld,并返回 node。

將這個文本類型的 node 添加進 p 標簽的 children 屬性后,此時 p 標簽的子節(jié)點解析完畢,彈出祖先棧,完成結(jié)束標簽的解析后,返回 p 標簽對應的 element 對象。

p 標簽對應的 node 節(jié)點生成,并在 parseChildren 函數(shù)中返回對應 node。

div 標簽在接收到 p 標簽的 node 后,添加進自身的 children 屬性中,出棧。此時祖先棧中就空空如也了。而 div 的標簽完成閉合解析的邏輯后,返回 element 元素。

最終 parseChildren 的第一次調(diào)用返回結(jié)果,生成了 div 對應的 node 對象,也返回了結(jié)果,將這個結(jié)果作為 createRoot 函數(shù)的 children 參數(shù)傳入,生成根節(jié)點對象,完成 ast 解析。

到此這篇關(guān)于Vue3 AST解析器 源碼解析的文章就介紹到這了,更多相關(guān)Vue3 AST解析器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue動態(tài)加載ECharts圖表數(shù)據(jù)的方式

    Vue動態(tài)加載ECharts圖表數(shù)據(jù)的方式

    這篇文章主要介紹了Vue動態(tài)加載ECharts圖表數(shù)據(jù)的方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • Vue2.0+Vux搭建一個完整的移動webApp項目的示例

    Vue2.0+Vux搭建一個完整的移動webApp項目的示例

    這篇文章主要介紹了Vue2.0+Vux搭建一個完整的移動webApp項目的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • 解決vue路由發(fā)生了跳轉(zhuǎn)但是界面沒有任何反應問題

    解決vue路由發(fā)生了跳轉(zhuǎn)但是界面沒有任何反應問題

    這篇文章主要介紹了解決vue路由發(fā)生了跳轉(zhuǎn)但是界面沒有任何反應問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • vue 中this.$set 動態(tài)綁定數(shù)據(jù)的案例講解

    vue 中this.$set 動態(tài)綁定數(shù)據(jù)的案例講解

    這篇文章主要介紹了vue 中this.$set 動態(tài)綁定數(shù)據(jù)的案例講解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • Vue動態(tài)組件?component?:is的使用代碼示范

    Vue動態(tài)組件?component?:is的使用代碼示范

    vue?動態(tài)組件用于實現(xiàn)在指定位置上,動態(tài)加載不同的組件,這篇文章主要介紹了Vue動態(tài)組件?component?:is的使用,需要的朋友可以參考下
    2023-09-09
  • vue父子組件的數(shù)據(jù)傳遞示例

    vue父子組件的數(shù)據(jù)傳遞示例

    這篇文章主要介紹了vue父子組件的數(shù)據(jù)傳遞示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-03-03
  • vant自定義二級菜單操作

    vant自定義二級菜單操作

    這篇文章主要介紹了vant自定義二級菜單操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-11-11
  • Vue懸浮窗和聚焦登錄組件功能實現(xiàn)

    Vue懸浮窗和聚焦登錄組件功能實現(xiàn)

    這篇文章主要介紹了Vue懸浮窗和聚焦登錄組件經(jīng)驗總結(jié),? 本文整理了實現(xiàn)懸浮窗以及聚焦登錄組件的功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-11-11
  • ejsExcel模板在Vue.js項目中的實際運用

    ejsExcel模板在Vue.js項目中的實際運用

    這篇文章主要介紹了ejsExcel模板在Vue.js項目中的實際運用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • vue數(shù)據(jù)更新但視圖(DOM)不刷新的幾種解決辦法

    vue數(shù)據(jù)更新但視圖(DOM)不刷新的幾種解決辦法

    這篇文章主要給大家介紹了關(guān)于vue數(shù)據(jù)更新但視圖(DOM)不刷新的幾種解決辦法,我們在開發(fā)過程中經(jīng)常會碰到數(shù)據(jù)更新,但是視圖并未改變的情況,需要的朋友可以參考下
    2023-08-08

最新評論