天天看點

Vue源碼解析(模闆編譯篇六)

代碼生成階段

1.前言

  • 前幾篇文章,我們把使用者所寫的模闆字元串先經過解析階段解析生成對應的抽象文法樹AST,接着再經過優化階段将AST中的靜态節點及靜态根節點都打上标記,現在終于到了模闆編譯三大階段的最後一個階段了——代碼生成階段。所謂代碼生成階段,到底是要生成什麼代碼?
    • 答:要生成render函數字元串。
  • 我們知道Vue執行個體在挂載的時候會調用其自身的render函數來生成執行個體上的template選項所對應的VNode,簡單的來說就是Vue隻要調用了render函數,就可以把模闆轉換成對應的虛拟DOM。那麼Vue要想調用render函數,那必須要先有這個render函數,那這個render函數又是從哪來的呢?是使用者手寫的還是Vue自己生成的?
    • 答案是都有可能。我們知道,我們在日常開發中是可以在Vue元件選項中手寫一個render選項,其值對應一個函數,那這個函數就是render函數,當使用者手寫了render函數時,那麼Vue在挂載該元件的時候就會調用使用者手寫的這個render函數。
  • 那如果使用者沒有寫呢?
    • 那這個時候Vue就要自己根據模闆内容生成一個render函數供元件挂載的時候調用。而Vue自己根據模闆内容生成render函數的過程就是本篇文章所要介紹的代碼生成階段。
  • 現在我們知道了,所謂代碼生成其實就是根據模闆對應的抽象文法樹AST生成一個函數,通過調用這個函數就可以得到模闆對應的虛拟DOM。

2.如何根據AST生成render函數

  • 上文我們知道了,代碼生成階段主要的工作就是根據已有的AST生成對應的render函數供元件挂載時調用,元件隻要調用的這個render函數就可以得到AST對應的虛拟DOM的VNode。那麼如何根據AST生成render函數呢?這其中是怎樣一個過程呢?接下來我們就來細細剖析一下。
  • 假設現有如下模闆:
  • 該模闆經過解析并優化後對應的AST如下:
    ast = {
        'type': 1,
        'tag': 'div',
        'attrsList': [
            {
                'name':'id',
                'value':'NLRX',
            }
        ],
        'attrsMap': {
          'id': 'NLRX',
        },
        'static':false,
        'parent': undefined,
        'plain': false,
        'children': [{
          'type': 1,
          'tag': 'p',
          'plain': false,
          'static':false,
          'children': [
            {
                'type': 2,
                'expression': '"Hello "+_s(name)',
                'text': 'Hello {{name}}',
                'static':false,
            }
          ]
        }]
      }
               
  • 下面我們就來根據已有的這個AST來生成對應的render函數。生成render函數的過程其實就是一個遞歸的過程,從頂向下依次遞歸AST中的每一個節點,根據不同的AST節點類型建立不同的VNode類型。接下來我們就來對照已有的模闆和AST實際示範一下生成render函數的過程。
    1. 首先,根節點div是一個元素型AST節點,那麼我們就要建立一個元素型VNode,我們把建立元素型VNode的方法叫做_c(tagName,data,children)。我們暫且不管_c()是什麼,隻需知道調用_c()就可以建立一個元素型VNode。那麼就可以生成如下代碼:
    2. 根節點div有子節點,那麼我們進入子節點清單children裡周遊子節點,發現子節點p也是元素型的,那就繼續建立元素型VNode并将其放入上述代碼中根節點的子節點清單中,如下:
    3. 同理,繼續周遊p節點的子節點,發現是一個文本型節點,那就建立一個文本型VNode并将其插入到p節點的子節點清單中,同理,建立文本型VNode我們調用_v()方法,如下:
    4. 到此,整個AST就周遊完畢了,我們将得到的代碼再包裝一下,如下:
      `
      with(this){
          reurn _c(
              'div',
              {
                  attrs:{"id":"NLRX"},
              },
              [
                  _c('p'),
                  [
                      _v("Hello "+_s(name))
                  ]
              ])
      }
      `
                 
    5. 最後,我們将上面得到的這個函數字元串傳遞給createFunction函數(關于這個函數在後面會介紹到),createFunction函數會幫我們把得到的函數字元串轉換成真正的函數,賦給元件中的render選項,進而就是render函數了。如下:
      res.render = createFunction(compiled.render, fnGenErrors)
      
      function createFunction (code, errors) {
        try {
          return new Function(code)
        } catch (err) {
          errors.push({ err, code })
          return noop
        }
      }
                 
  • 以上就是根據一個簡單的模闆所對應的AST生成render函數的過程,理論過程我們已經了解了,那麼在源碼中實際是如何實作的呢?下面我們就回歸源碼分析其具體實作過程。

3.回歸源碼

  • 代碼生成階段的源碼位于src/compiler/codegen/index.js 中,源碼雖然很長,但是邏輯不複雜,核心邏輯如下:
    export function generate (ast,option) {
      const state = new CodegenState(options)
      const code = ast ? genElement(ast, state) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
               
  • 調用generate函數并傳入優化後得到的ast,在generate函數内部先判斷ast是否為空,不為空則調用genElement(ast, state)函數建立VNode,為空則建立一個空的元素型div的VNode。然後将得到的結果用with(this){return ${code}}包裹傳回。可以看出,真正起作用的是genElement函數,下面我們繼續來看一下genElement函數内部是怎樣的。
  • genElement函數定義如下:
    export function genElement (el: ASTElement, state: CodegenState): string {
      if (el.staticRoot && !el.staticProcessed) {
        return genStatic(el, state)
      } else if (el.once && !el.onceProcessed) {
        return genOnce(el, state)
      } else if (el.for && !el.forProcessed) {
        return genFor(el, state)
      } else if (el.if && !el.ifProcessed) {
        return genIf(el, state)
      } else if (el.tag === 'template' && !el.slotTarget) {
        return genChildren(el, state) || 'void 0'
      } else if (el.tag === 'slot') {
        return genSlot(el, state)
      } else {
        // component or element
        let code
        if (el.component) {
          code = genComponent(el.component, el, state)
        } else {
          const data = el.plain ? undefined : genData(el, state)
    
          const children = el.inlineTemplate ? null : genChildren(el, state, true)
          code = `_c('${el.tag}'${
            data ? `,${data}` : '' // data
          }${
            children ? `,${children}` : '' // children
          })`
        }
        // module transforms
        for (let i = 0; i < state.transforms.length; i++) {
          code = state.transforms[i](el, code)
        }
        return code
      }
    }
               
  • genElement函數邏輯很清晰,就是根據目前 AST 元素節點屬性的不同進而執行不同的代碼生成函數。雖然元素節點屬性的情況有很多種,但是最後真正建立出來的VNode無非就三種,分别是元素節點,文本節點,注釋節點。接下來我們就着重分析一下如何生成這三種節點類型的render函數的。
3.1 元素節點
  • 生成元素型節點的render函數代碼如下:
    const data = el.plain ? undefined : genData(el, state)
    
    const children = el.inlineTemplate ? null : genChildren(el, state, true)
    code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
    }${
    children ? `,${children}` : '' // children
    })`
               
  • 生成元素節點的render函數就是生成一個_c()函數調用的字元串,上文提到了_c()函數接收三個參數,分别是節點的标簽名tagName,節點屬性data,節點的子節點清單children。那麼我們隻需将這三部分都填進去即可。
    1. 擷取節點屬性data

      首先判斷plain屬性是否為true,若為true則表示節點沒有屬性,将data指派為undefined;如果不為true則調用genData函數擷取節點屬性data資料。genData函數定義如下:

      export function genData (el: ASTElement, state: CodegenState): string {
        let data = '{'
        const dirs = genDirectives(el, state)
        if (dirs) data += dirs + ','
      
          // key
          if (el.key) {
              data += `key:${el.key},`
          }
          // ref
          if (el.ref) {
              data += `ref:${el.ref},`
          }
          if (el.refInFor) {
              data += `refInFor:true,`
          }
          // pre
          if (el.pre) {
              data += `pre:true,`
          }
          // 篇幅所限,省略其他情況的判斷
          data = data.replace(/,$/, '') + '}'
          return data
      }
                 
      我們看到,源碼中genData雖然很長,但是其邏輯非常簡單,就是在拼接字元串,先給data指派為一個{,然後判斷存在哪些屬性資料,就将這些資料拼接到data中,最後再加一個},最終得到節點全部屬性data。
    2. 擷取子節點清單children

      擷取子節點清單children其實就是周遊AST的children屬性中的元素,然後根據元素屬性的不同生成不同的VNode建立函數調用字元串,如下:

      export function genChildren (el):  {
          if (children.length) {
              return `[${children.map(c => genNode(c, state)).join(',')}]`
          }
      }
      function genNode (node: ASTNode, state: CodegenState): string {
        if (node.type === 1) {
          return genElement(node, state)
        } if (node.type === 3 && node.isComment) {
          return genComment(node)
        } else {
          return genText(node)
        }
      }
                 
    3. 上面兩步完成之後,生成_c()函數調用字元串,如下:
      code = `_c('${el.tag}'${
              data ? `,${data}` : '' // data
            }${
              children ? `,${children}` : '' // children
            })`
                 
3.2文本節點
  • 文本型的VNode可以調用_v(text)函數來建立,是以生成文本節點的render函數就是生成一個_v(text)函數調用的字元串。_v()函數接收文本内容作為參數,如果文本是動态文本,則使用動态文本AST節點的expression屬性,如果是純靜态文本,則使用text屬性。其生成代碼如下:
    export function genText (text: ASTText | ASTExpression): string {
      return `_v(${text.type === 2
        ? text.expression // no need for () because already wrapped in _s()
        : transformSpecialNewlines(JSON.stringify(text.text))
      })`
    }
               
3.3注釋節點
  • 注釋型的VNode可以調用_e(text)函數來建立,是以生成注釋節點的render函數就是生成一個_e(text)函數調用的字元串。_e()函數接收注釋内容作為參數,其生成代碼如下:
    export function genComment (comment: ASTText): string {
      return `_e(${JSON.stringify(comment.text)})`
    }
               

4.總結

  • 本篇文章介紹了模闆編譯三大階段的最後一個階段——代碼生成階段。
    • 首先,介紹了為什麼要有代碼生成階段以及代碼生成階段主要幹什麼。我們知道了,代碼生成其實就是根據模闆對應的抽象文法樹AST生成一個函數供元件挂載時調用,通過調用這個函數就可以得到模闆對應的虛拟DOM。
    • 接着,我們通過一個簡單的模闆示範了把模闆經過遞歸周遊最後生成render函數的過程。
    • 最後,我們回歸源碼,通過分析源碼了解了生成render函數的具體實作過程。

下一篇總結

繼續閱讀