91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue3中AST解析器的示例分析

發布時間:2021-09-26 09:56:10 來源:億速云 閱讀:132 作者:小新 欄目:開發技術

小編給大家分享一下Vue3中AST解析器的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

1、生成 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 的函數中我添加了注釋,方便大家理解各個函數的作用,首先會創建解析的上下文,之后根據上下文獲取游標信息,由于還未進行解析,所以游標中的 columnlineoffset 屬性對應的都是 template 的起始位置。之后就是創建根節點并返回根節點,至此ast 樹生成,解析完成。

2、創建 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 函數,還是老規矩,為了幫助大家理解,我會精簡函數體內的邏輯。

3、解析子節點

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)
  }
}

4、解析模板元素 Element

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 對象。

5、示例:模板元素解析

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

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

Vue3中AST解析器的示例分析

圖中的黃色矩形是一個棧,當開始解析時,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解析器的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

广汉市| 渝北区| 安多县| 岱山县| 灵武市| 彩票| 双江| 新和县| 福海县| 岳池县| 怀宁县| 门头沟区| 马龙县| 辽阳县| 宣汉县| 梨树县| 盐亭县| 澜沧| 平湖市| 扶风县| 肇州县| 齐河县| 望谟县| 平凉市| 嘉义县| 南宫市| 黄浦区| 奉贤区| 乐清市| 冀州市| 丹阳市| 麻阳| 肃北| 报价| 贞丰县| 鄢陵县| 饶河县| 湘西| 瑞金市| 综艺| 龙川县|