Vue.js 3.0編譯器compiler-core源碼解析

做者:深山螞蟻html

Vue3的源代碼正在國慶假期就這麼忽然放出來了,假期還沒結束,陸陸續續看到努力的碼農就在各類分析了。vue

目前仍是 pre Alpha,樂觀估計還有 Alpha,Beta版本,最後纔是正式版。node

話很少說,看 Pre-Alpha。 瞧 compiler-corereact

熱門的 reactivity 被大佬翻來覆去再研究了,我就和大夥一塊兒來解讀一下 」冷門「 的 compiler 吧!😄😄😄😄git

若是你對 AST 還不太熟悉,或者對如何實現一個簡單的 AST解析器 還不太熟悉,能夠猛戳:手把手教你寫一個 AST 解析器github

vue3.0的模板解析和vue2.0差別比較大,可是不管怎樣變化,基本原理是一致的,咱們寫的各類 html 代碼,js使用的時候其實就是一個字符串,將非結構化的字符串數據,轉換成結構化的 AST,咱們都是使用強大的正則表達式和indexOf來判斷。
compiler-core 的一個核心做用就是將字符串轉換成 抽象對象語法樹AST。正則表達式

Let's do IT !微信

目錄結構

  • _tests_ 測試用例
  • src/ast ts語法的大佬的類型定義,好比type,enum,interface等
  • src/codegen 將生成的ast轉換成render字符串
  • src/errors 定義 compiler 錯誤類型
  • src/index 入口文件,主要有一個 baseCompile ,用來編譯模板文件的
  • src/parse 將模板字符串轉換成 AST
  • src/runtimeHelper 生成code的時候的定義常量對應關係
  • src/transform 處理 AST 中的 vue 特有語法,好比 v-if ,v-on 的解析

進入 compiler-core 目錄下,結構一目瞭然。這裏說下 _tests_ 目錄,是vue的jest測試用例。
閱讀源碼前先看看用例,對閱讀源碼有很大幫助哦。ide

以下,測試一個簡單的text,執行parse方法以後,獲得 ast,指望 ast 的第一個節點與定義的對象是一致的。
同理其餘的模塊測試用例,在閱讀源碼前能夠先瞄一眼,知道這個模塊如何使用,輸入輸出是啥。post

test('simple text', () => {
    const ast = parse('some text')
    const text = ast.children[0] as TextNode
    expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        isEmpty: false,
        loc: {
            start: { offset: 0, line: 1, column: 1 },
            end: { offset: 9, line: 1, column: 10 },
            source: 'some text'
        }
    })
})
複製代碼

先看一張圖,重點是四塊:

  • 起始標籤
  • 結束標籤
  • 動態內容
  • 普通內容

其中起始標籤會用到遞歸來處理子節點。

alt

接下來,咱們開始跟着源碼來閱讀吧~~~~~~

parse:將字符串模板轉換成 AST 抽象語法樹

這個是對外暴露的核心方法,咱們先測試下結果:

const source = ` <div id="test" :class="cls"> <span>{{ name }}</span> <MyCom></MyCom> </div> `.trim()
import { parse } from './compiler-core.cjs'
const result = parse(source)
複製代碼

output:

output

一個簡單的轉換結果就呈現出來了,從生成的結構來看,相對於vue2.x有幾個比較重要的變化:

  • 新增了 loc 屬性 每個節點都記錄了該節點在源碼當中的 start 和 end,標識了代碼的詳細位置,column,line,offset。
    vu3.0對於開發遇到的問題都要詳細的日誌輸出也基於此,另外支持 source-map
  • 新增了 tagType 屬性
    tagType 屬性標識該節點是什麼類型的。咱們知道 vue2.x 判斷節點類型是運行時纔有的,vu3.0將判斷提早到編譯階段了,提高了性能。
    目前tagType有三種類型:0 element,1 component,2 slot,3 template
  • 新增 isStatic 屬性
    將模板提早編譯好,標識是否爲動態變化的,好比動態指令
  • ……

新版的 AST 明顯比 vue2.x 要複雜些,能夠看到vue3.0將不少能夠在編譯階段就能肯定的就在編譯階段肯定,標識編譯結果,不須要等到運行時再去判斷,節省內存和性能。這個也是尤大大重點說了的,優化編譯,提高性能。

接下來咱們來看下轉換的代碼,主要有以下幾個方法:

  • parse & parseChildren 主入口
  • parseTag 處理標籤
  • parseAttribute 處理標籤上的屬性
  • parseElement 處理起始標籤
  • parseInterpolation 處理動態文本內容
  • parseText 處理靜態文本內容

parse & parseChildren 主入口

parse 的主入口,這裏建立了一個 parseContext,有利於後續直接從 context 上拿到 content,options 等。
getCursor 獲取當前處理的指針位置,用戶生成 loc,初始都是1。

export function parse(content: string, options: ParserOptions = {}): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return {
    type: NodeTypes.ROOT,
    children: parseChildren(context, TextModes.DATA, []),
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    codegenNode: undefined,
    loc: getSelection(context, start)
  }
}
複製代碼

重點看下 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)) {
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
    if (startsWith(s, context.options.delimiters[0])) {
      // '{{'
      node = parseInterpolation(context, mode)
    } else if (mode === TextModes.DATA && s[0] === '<') {
      // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
      if (s.length === 1) {
        emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
      } else if (s[1] === '!') {
          // <!DOCTYPE <![CDATA[ 等非節點元素 暫不討論
      } else if (s[1] === '/') {
        if (s.length === 2) {
        } else if (s[2] === '>') {
          advanceBy(context, 3)
          continue
        } else if (/[a-z]/i.test(s[2])) {
          parseTag(context, TagType.End, parent)
          continue
        } else {
        }
      } else if (/[a-z]/i.test(s[1])) {
        node = parseElement(context, ancestors)
      } else if (s[1] === '?') {
      } else {
      }
    }
    if (!node) {
      node = parseText(context, mode)
    }
    if (Array.isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(context, nodes, node[i])
      }
    } else {
      pushNode(context, nodes, node)
    }
  }
  return nodes
}
複製代碼

ancestors 用來存儲未匹配的起始節點,爲後進先出的stack。

循環處理 source,循環截止條件是 isEnd 方法返回true,便是處理完成了,結束有兩個條件:

  1. context.source爲空,即整個模板都處理完成
  2. 碰到截止節點標籤(</),且能在未匹配的起始標籤(ancestors)裏面找到對對應的tag。這個對應 parseChildren 的子節點處理完成。

匹配還沒有結束,則進入循環匹配。有三種狀況:

  1. if (startsWith(s, context.options.delimiters[0]))
    delimiters 是分割符合,vue 是 {{ 和 }} 。開始匹配到vue的文本輸出內容 {{ ,則意味着須要處理 文本內容插入,
  2. else if (mode === TextModes.DATA && s[0] === '<')
    內容是已 < 開頭,即 html 標籤的標識符號,則開始處理起始標籤和截止標籤兩種狀況。
  3. 以上條件都不是,或者匹配未成功
    那麼就是動態文本內容了。

若是是第三種動態文本插入,則執行 parseInterpolation 組裝文本節點,其中 isStatic=false 標識是變量,比較簡答,方法就不貼了。

return {
    type: NodeTypes.INTERPOLATION,
    content: {
      type: NodeTypes.SIMPLE_EXPRESSION,
      isStatic: false,
      content,
      loc: getSelection(context, innerStart, innerEnd)
    },
    loc: getSelection(context, start)
  }
複製代碼

再看下這兩個處理 source 內容後移的方法:

advanceBy(context,number) : 將須要處理的模板source ,後移 number 個字符,從新記錄 loc
advanceSpaces() : 後移存在的連續的空格

回到上面的匹配條件,若是是 < 開頭,分兩種狀況:

  1. 第二個字符是 "/"
    對應的就是 </
    若是是 </> ,那麼認爲是一個無效標籤,直接 advanceBy 後移 3 個字符便可。
    若是是 </a,那麼認爲是一個截止標籤,執行 parseTag 方法處理。
  2. 第二個字符是字母
    對應就是標籤的起始文字了,如 <\div,執行 parseElement 方法處理起始標籤。

parseTag 處理標籤

若是是截止標籤:parseTag,則直接處理完成。
若是是起始標籤:parseElement 執行,調用parseTag 處理標籤,而後再去遞歸處理子節點等。

正則:/^</?([a-z][^\t\r\n\f />]*)/i 這個就很少說了,匹配 <\div> </div>這種標籤。
測試 match :

/^<\/?([a-z][^\t\r\n\f />]*)/i.exec("<div class='abc'>")
(2) ["<div", "div", index: 0, input: "<div class='abc'>", groups: undefined]
複製代碼

顯然,mathch[1] 即匹配到的標籤元素。咱們看主方法:

function parseTag( context: ParserContext, type: TagType, parent: ElementNode | undefined ): ElementNode {
  // Tag open.
  const start = getCursor(context)
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  const tag = match[1]
  const props = []
  const ns = context.options.getNamespace(tag, parent)
  let tagType = ElementTypes.ELEMENT
  if (tag === 'slot') tagType = ElementTypes.SLOT
  else if (tag === 'template') tagType = ElementTypes.TEMPLATE
  else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
  advanceBy(context, match[0].length)
  advanceSpaces(context)
  // Attributes.
  const attributeNames = new Set<string>()
  while (
    context.source.length > 0 &&
    !startsWith(context.source, '>') &&
    !startsWith(context.source, '/>')
  ) {
    const attr = parseAttribute(context, attributeNames)
    if (type === TagType.Start) {
      props.push(attr)
    }
    advanceSpaces(context)
  }
  // Tag close.
  let isSelfClosing = false
  if (context.source.length === 0) {
  } else {
    isSelfClosing = startsWith(context.source, '/>')
    advanceBy(context, isSelfClosing ? 2 : 1)
  }
  return {
    type: NodeTypes.ELEMENT,
    ns,
    tag,
    tagType,
    props,
    isSelfClosing,
    children: [],
    loc: getSelection(context, start),
    codegenNode: undefined // to be created during transform phase
  }
}
複製代碼

tagType有四種類型,在這裏定義了,分別是: 0 element,1 component,2 slot,3 template

咱們看while 循環,advanceBy 去掉起始 < 和標籤名以後:
若是跟着是 > 或者 /> ,那麼標籤處理結束,退出循環。
不然是標籤的元素,咱們執行 parseAttribute 來處理標籤屬性,該節點上增長props,保存 該起始節點的 attributes;

執行方法後面的!,是ts語法,至關於告訴ts,這裏必定會有值,無需作空判斷,如 const match = /^</?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!

parseAttribute 處理標籤上的屬性

正則獲取屬性上的name

/^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec('class='abc'>')
["class", index: 0, input: "class='abc'>", groups: undefined]
複製代碼

若是不是一個孤立的屬性,有value值的話(/[1]*=/.test(context.source)),那麼再獲取屬性的value。

function parseAttribute( context: ParserContext, nameSet: Set<string> ): AttributeNode | DirectiveNode {
  // Name.
  const start = getCursor(context)
  const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
  const name = match[0]
  nameSet.add(name)
  advanceBy(context, name.length)
  // Value
  let value:
    | {
        content: string
        isQuoted: boolean
        loc: SourceLocation
      }
    | undefined = undefined
  if (/^[\t\r\n\f ]*=/.test(context.source)) {
    advanceSpaces(context)
    advanceBy(context, 1)
    advanceSpaces(context)
    value = parseAttributeValue(context)
  }
  const loc = getSelection(context, start)
  if (/^(v-|:|@|#)/.test(name)) {
    const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
      name
    )!
    let arg: ExpressionNode | undefined
    if (match[2]) {
      const startOffset = name.split(match[2], 2)!.shift()!.length
      const loc = getSelection(
        context,
        getNewPosition(context, start, startOffset),
        getNewPosition(context, start, startOffset + match[2].length)
      )
      let content = match[2]
      let isStatic = true

      if (content.startsWith('[')) {
        isStatic = false
        content = content.substr(1, content.length - 2)
      }
      arg = {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content,
        isStatic,
        loc
      }
    }
    if (value && value.isQuoted) {
      const valueLoc = value.loc
      valueLoc.start.offset++
      valueLoc.start.column++
      valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
      valueLoc.source = valueLoc.source.slice(1, -1)
    }
    return {
      type: NodeTypes.DIRECTIVE,
      name:
        match[1] ||
        (startsWith(name, ':')
          ? 'bind'
          : startsWith(name, '@')
            ? 'on'
            : 'slot'),
      exp: value && {
        type: NodeTypes.SIMPLE_EXPRESSION,
        content: value.content,
        isStatic: false,
        loc: value.loc
      },
      arg,
      modifiers: match[3] ? match[3].substr(1).split('.') : [],
      loc
    }
  }
  return {
    type: NodeTypes.ATTRIBUTE,
    name,
    value: value && {
      type: NodeTypes.TEXT,
      content: value.content,
      isEmpty: value.content.trim().length === 0,
      loc: value.loc
    },
    loc
  }
}
複製代碼

parseAttributeValue 獲取屬性值的方法比較容易:

  • 若是value值有引號開始,那麼就找到下一個引號未value值結束 (class="aaa" class='aaa')
  • 若是value沒有引號,那麼就找到下一個空格爲value值結束 (class=aaa)

其中有處理vue的語法特性,若是屬性名稱是v-,:,@,#開頭的,須要特殊處理,看下這個正則:

/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec("v-name")
(4) ["v-name", "name", undefined, undefined, index: 0, input: "v-name", groups: undefined]

/(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(":name")
(4) [":name", undefined, "name", undefined, index: 0, input: ":name", groups: undefined]
複製代碼

mathch[2]若是有值,即匹配到了,說明是非 v-name,若是是名稱是[]包裹的則是 動態指令, 將 isStatic 置爲 false

parseElement 處理起始標籤

parseElement 處理起始標籤,咱們先執行 parseTag 解析標籤,獲取到起始節點的 標籤元素和屬性,若是當前也是截止標籤(好比
),則直接返回該標籤。
不然,將起始標籤 push 到未匹配的起始 ancestors棧裏面。
而後繼續去處理子元素 parseChildren ,注意,將未匹配的 ancestors 傳進去了,parseChildren 的截止條件有兩個:

  1. context.source爲空,即處理完成
  2. 碰到截止節點標籤(</),且能在未匹配的起始標籤(ancestors)裏面找到對對應的tag。

所以,若是是循環碰到匹配的截止標籤了,則須要 ancestors.pop(),將節點添加到當前的子節點。

固然,處理當前起始節點,該節點也多是截止節點,好比:<\img src="xxx"/>,則繼續去執行處理截止節點便可。
方法以下:

function parseElement( context: ParserContext, ancestors: ElementNode[] ): ElementNode | undefined {
  // Start tag.
  const parent = last(ancestors)
  const element = parseTag(context, TagType.Start, parent)

  if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
    return element
  }
  // Children.
  ancestors.push(element)
  const mode = (context.options.getTextMode(
    element.tag,
    element.ns
  ) as unknown) as TextModes
  const children = parseChildren(context, mode, ancestors)
  ancestors.pop()
  element.children = children
  // End tag.
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End, parent)
  } else {
  }
  element.loc = getSelection(context, element.loc.start)
  return element
}
複製代碼

至此,vue3.0的 將 模板文件轉換成 AST 的主流程已經基本完成。
靜待下篇,AST 的 transform 處理。


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam


  1. \t\r\n\f ↩︎

相關文章
相關標籤/搜索