看不懂來打我,vue3如何將template編譯成render函式

前端欧阳發表於2024-04-12

前言

在之前的 透過debug搞清楚.vue檔案怎麼變成.js檔案 文章中我們講過了vue檔案是如何編譯成js檔案,透過那篇文章我們知道了,template編譯為render函式底層就是呼叫了@vue/compiler-sfc包暴露出來的compileTemplate函式。由於文章篇幅有限,我們沒有去深入探索compileTemplate函式是如何將template模組編譯為render函式,在這篇文章中我們來了解一下。

@vue下面的幾個包

先來介紹一下本文中涉及到vue下的幾個包,分別是:@vue/compiler-sfc@vue/compiler-dom@vue/compiler-core

  • @vue/compiler-sfc:用於編譯vue的SFC檔案,這個包依賴vue下的其他包,比如@vue/compiler-dom@vue/compiler-core。這個包一般是給vue-loader@vitejs/plugin-vue使用的。

  • @vue/compiler-dom:這個包專注於瀏覽器端的編譯,處理瀏覽器dom相關的邏輯都在這裡面。

  • @vue/compiler-core:從名字你也能看出來這個包是vue編譯部分的核心,提供了通用的編譯邏輯,不管是瀏覽器端還是服務端編譯最終都會走到這個包裡面來。

先來看個流程圖

先來看一下我畫的template模組編譯為render函式這一過程的流程圖,讓你對整個流程有個大概的印象,後面的內容看著就不費勁了。如下圖:
full-progress

從上面的流程圖可以看到整個流程可以分為7步:

  • 執行@vue/compiler-sfc包的compileTemplate函式,裡面會呼叫同一個包的doCompileTemplate函式。

  • 執行@vue/compiler-sfc包的doCompileTemplate函式,裡面會呼叫@vue/compiler-dom包中的compile函式。

  • 執行@vue/compiler-dom包中的compile函式,裡面會對options進行了擴充套件,塞了一些處理dom的轉換函式進去。分別塞到了options.nodeTransforms陣列和options.directiveTransforms物件中。然後以擴充套件後的options去呼叫@vue/compiler-core包的baseCompile函式。

  • 執行@vue/compiler-core包的baseCompile函式,在這個函式中主要分為4部分。第一部分為檢查傳入的source是不是html字串,如果是就呼叫同一個包下的baseParse函式生成模版AST抽象語法樹。否則就直接使用傳入的模版AST抽象語法樹。此時node節點中還有v-forv-model等指令。這裡的模版AST抽象語法樹結構和template模組中的程式碼結構是一模一樣的,所以說模版AST抽象語法樹就是對template模組中的結構進行描述。

  • 第二部分為執行getBaseTransformPreset函式拿到@vue/compiler-core包中內建的nodeTransformsdirectiveTransforms轉換函式。

  • 第三部分為將傳入的options.nodeTransformsoptions.directiveTransforms分別和本地的nodeTransformsdirectiveTransforms進行合併得到一堆新的轉換函式,和模版AST抽象語法樹一起傳入到transform函式中執行,就會得到轉換後的javascript AST抽象語法樹。在這一過程中v-forv-model等指令已經被轉換函式給處理了。得到的javascript AST抽象語法樹的結構和將要生成的render函式的結構是一模一樣的,所以說javascript AST抽象語法樹就是對render函式的結構進行描述。

  • 第四部分為由於已經拿到了和render函式的結構一模一樣的javascript AST抽象語法樹,只需要在generate函式中遍歷javascript AST抽象語法樹進行字串拼接就可以得到render函式了。

關注公眾號:前端歐陽,解鎖我更多vue乾貨文章。還可以加我微信,私信我想看哪些vue原理文章,我會根據大家的反饋進行創作。
qrcode

@vue/compiler-sfc包的compileTemplate函式

還是同樣的套路,我們透過debug一個簡單的demo來搞清楚compileTemplate函式是如何將template編譯成render函式的。demo程式碼如下:

<template>
  <input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const msgList = ref([
  {
    id: 1,
    value: "",
  },
  {
    id: 2,
    value: "",
  },
  {
    id: 3,
    value: "",
  },
]);
</script>

透過debug搞清楚.vue檔案怎麼變成.js檔案 文章中我們已經知道了在使用vite的情況下template編譯為render函式是在node端完成的。所以我們需要啟動一個debug終端,才可以在node端打斷點。這裡以vscode舉例,首先我們需要開啟終端,然後點選終端中的+號旁邊的下拉箭頭,在下拉中點選Javascript Debug Terminal就可以啟動一個debug終端。
debug-terminal

compileTemplate函式在node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js檔案中,找到compileTemplate函式打上斷點,然後在debug終端中執行yarn dev(這裡是以vite舉例)。在瀏覽器中訪問 http://localhost:5173/,此時斷點就會走到compileTemplate函式中了。在我們這個場景中compileTemplate函式簡化後的程式碼非常簡單,程式碼如下:

function compileTemplate(options) {
  return doCompileTemplate(options);
}

@vue/compiler-sfc包的doCompileTemplate函式

我們接著將斷點走進doCompileTemplate函式中,看看裡面的程式碼是什麼樣的,簡化後的程式碼如下:

import * as CompilerDOM from '@vue/compiler-dom'

function doCompileTemplate({
  source,
  ast: inAST,
  compiler
}) {
  const defaultCompiler = CompilerDOM;
  compiler = compiler || defaultCompiler;
  let { code, ast, preamble, map } = compiler.compile(inAST || source, {
    // ...省略傳入的options
  });
  return { code, ast, preamble, source, errors, tips, map };
}

doCompileTemplate函式中程式碼同樣也很簡單,我們在debug終端中看看compilersourceinAST這三個變數的值是長什麼樣的。如下圖:
doCompileTemplate

從上圖中我們可以看到此時的compiler變數的值為undefinedsource變數的值為template模組中的程式碼,inAST的值為由template模組編譯而來的AST抽象語法樹。不是說好的要經過parse函式處理後才會得到AST抽象語法樹,為什麼這裡就已經有了AST抽象語法樹?不要著急接著向下看,後面我會解釋。

由於這裡的compiler變數的值為undefined,所以compiler會被賦值為CompilerDOM。而CompilerDOM就是@vue/compiler-dom包中暴露的所有內容。執行compiler.compile函式,就是執行@vue/compiler-dom包中的compile函式。compile函式接收的第一個引數為inAST || source,從這裡我們知道第一個引數既可能是AST抽象語法樹,也有可能是template模組中的html程式碼字串。compile函式的返回值物件中的code欄位就是編譯好的render函式,然後return出去。

@vue/compiler-dom包中的compile函式

我們接著將斷點走進@vue/compiler-dom包中的compile函式,發現程式碼同樣也很簡單,簡化後的程式碼如下:

import {
  baseCompile,
} from '@vue/compiler-core'

function compile(src, options = {}) {
  return baseCompile(
    src,
    Object.assign({}, parserOptions, options, {
      nodeTransforms: [
        ...DOMNodeTransforms,
        ...options.nodeTransforms || []
      ],
      directiveTransforms: shared.extend(
        {},
        DOMDirectiveTransforms,
        options.directiveTransforms || {}
      )
    })
  );
}

從上面的程式碼中可以看到這裡的compile函式也不是具體實現的地方,在這裡呼叫的是@vue/compiler-core包的baseCompile函式。看到這裡你可能會有疑問,為什麼不在上一步的doCompileTemplate函式中直接呼叫@vue/compiler-core包的baseCompile函式,而是要從@vue/compiler-dom包中繞一圈再來呼叫呢baseCompile函式呢?

答案是baseCompile函式是一個處於@vue/compiler-core包中的API,而@vue/compiler-core可以執行在各種 JavaScript 環境下,比如瀏覽器端、服務端等各個平臺。baseCompile函式接收這些平臺專有的一些options,而我們這裡的demo是瀏覽器平臺。所以才需要從@vue/compiler-dom包中繞一圈去呼叫@vue/compiler-core包中的baseCompile函式傳入一些瀏覽器中特有的options。在上面的程式碼中我們看到使用DOMNodeTransforms陣列對options中的nodeTransforms屬性進行了擴充套件,使用DOMDirectiveTransforms物件對options中的directiveTransforms屬性進行了擴充套件。

我們先來看看DOMNodeTransforms陣列:

const DOMNodeTransforms = [
  transformStyle
];

options物件中的nodeTransforms屬性是一個陣列,裡面包含了許多transform轉換函式用於處理AST抽象語法樹。經過@vue/compiler-domcompile函式處理後nodeTransforms陣列中多了一個處理style的transformStyle函式。這裡的transformStyle是一個轉換函式用於處理dom上面的style,比如style="color: red"

我們再來看看DOMDirectiveTransforms物件:

const DOMDirectiveTransforms = {
  cloak: compilerCore.noopDirectiveTransform,
  html: transformVHtml,
  text: transformVText,
  model: transformModel,
  on: transformOn,
  show: transformShow
};

options物件中的directiveTransforms屬性是一個物件,經過@vue/compiler-domcompile函式處理後directiveTransforms物件中增加了處理v-cloakv-htmlv-textv-modelv-onv-show等指令的transform轉換函式。很明顯我們這個demo中input標籤上面的v-model指令就是由這裡的transformModel轉換函式處理。

你發現了沒,不管是nodeTransforms陣列還是directiveTransforms物件,增加的transform轉換函式都是處理dom相關的。經過@vue/compiler-domcompile函式處理後,再呼叫baseCompile函式就有了處理dom相關的轉換函式了。

@vue/compiler-core包的baseCompile函式

繼續將斷點走進vue/compiler-core包的baseCompile函式,簡化後的baseCompile函式程式碼如下:

function baseCompile(
  source: string | RootNode,
  options: CompilerOptions = {},
): CodegenResult {
  const ast = isString(source) ? baseParse(source, options) : source

  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()

  transform(
    ast,
    Object.assign({}, options, {
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []), // user transforms
      ],
      directiveTransforms: Object.assign(
        {},
        directiveTransforms,
        options.directiveTransforms || {}, // user transforms
      ),
    }),
  )

  return generate(ast, options)
}

我們先來看看baseCompile函式接收的引數,第一個引數為source,型別為string | RootNode。這句話的意思是接收的source變數可能是html字串,也有可能是html字串編譯後的AST抽象語法樹。再來看看第二個引數options,我們這裡只關注options.nodeTransforms陣列屬性和options.directiveTransforms物件屬性,這兩個裡面都是存了一堆轉換函式,區別就是一個是陣列,一個是物件。

我們再來看看返回值型別CodegenResult,定義如下:

interface CodegenResult {
  code: string
  preamble: string
  ast: RootNode
  map?: RawSourceMap
}

從型別中我們可以看到返回值物件中的code屬性就是編譯好的render函式,而這個返回值就是最後呼叫generate函式返回的。

明白了baseCompile函式接收的引數和返回值,我們再來看函式內的程式碼。主要分為四塊內容:

  • 拿到由html字串轉換成的AST抽象語法樹。

  • 拿到由一堆轉換函式組成的nodeTransforms陣列,和拿到由一堆轉換函式組成的directiveTransforms物件。

  • 執行transform函式,使用合併後的nodeTransforms中的所有轉換函式處理AST抽象語法樹中的所有node節點,使用合併後的directiveTransforms中的轉換函式對會生成props的指令進行處理,得到處理後的javascript AST抽象語法樹

  • 呼叫generate函式根據上一步處理後的javascript AST抽象語法樹進行字串拼接,拼成render函式。

獲取AST抽象語法樹

我們先來看第一塊的內容,程式碼如下:

const ast = isString(source) ? baseParse(source, options) : source

如果傳入的source是html字串,那就呼叫baseParse函式根據html字串生成對應的AST抽象語法樹,如果傳入的就是AST抽象語法樹那麼就直接賦值給ast變數。為什麼這裡有這兩種情況呢?

原因是baseCompile函式可以被直接呼叫,也可以像我們這樣由vite的@vitejs/plugin-vue包發起,經過層層呼叫後最終執行baseCompile函式。在我們這個場景中,在前面我們就知道了走進compileTemplate函式之前就已經有了編譯後的AST抽象語法樹,所以這裡不會再呼叫baseParse函式去生成AST抽象語法樹了。那麼又是什麼時候生成的AST抽象語法樹呢?

在之前的 透過debug搞清楚.vue檔案怎麼變成.js檔案 文章中我們講了呼叫createDescriptor函式會將vue程式碼字串轉換為descriptor物件,descriptor物件中擁有template屬性、scriptSetup屬性、styles屬性,分別對應vue檔案中的template模組、<script setup>模組、<style>模組。如下圖:
progress-createDescriptor
createDescriptor函式在生成template屬性的時候底層同樣也會呼叫@vue/compiler-core包的baseParse函式,將template模組中的html字串編譯為AST抽象語法樹。

所以在我們這個場景中走到baseCompile函式時就已經有了AST抽象語法樹了,其實底層都呼叫的是@vue/compiler-core包的baseParse函式。

獲取轉換函式

接著將斷點走到第二塊內容處,程式碼如下:

const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()

從上面的程式碼可以看到getBaseTransformPreset函式的返回值是一個陣列,對返回的陣列進行解構,陣列的第一項賦值給nodeTransforms變數,陣列的第二項賦值給directiveTransforms變數。

將斷點走進getBaseTransformPreset函式,程式碼如下:

function getBaseTransformPreset() {
  return [
    [
      transformOnce,
      transformIf,
      transformMemo,
      transformFor,
      transformFilter,
      trackVForSlotScopes,
      transformExpression
      transformSlotOutlet,
      transformElement,
      trackSlotScopes,
      transformText
    ],
    {
      on: transformOn,
      bind: transformBind,
      model: transformModel
    }
  ];
}

從上面的程式碼中不難看出由getBaseTransformPreset函式的返回值解構出來的nodeTransforms變數是一個陣列,陣列中包含一堆transform轉換函式,比如處理v-oncev-ifv-memov-for等指令的轉換函式。很明顯我們這個demo中input標籤上面的v-for指令就是由這裡的transformFor轉換函式處理。

同理由getBaseTransformPreset函式的返回值解構出來的directiveTransforms變數是一個物件,物件中包含處理v-onv-bindv-model指令的轉換函式。

經過這一步的處理我們就拿到了由一系列轉換函式組成的nodeTransforms陣列,和由一系列轉換函式組成的directiveTransforms物件。看到這裡我想你可能有一些疑問,為什麼nodeTransforms是陣列,directiveTransforms卻是物件呢?為什麼有的指令轉換轉換函式是在nodeTransforms陣列中,有的卻是在directiveTransforms物件中呢?彆著急,我們下面會講。

transform函式

接著將斷點走到第三塊內容,transform函式處,程式碼如下:

transform(
  ast,
  Object.assign({}, options, {
    nodeTransforms: [
      ...nodeTransforms,
      ...(options.nodeTransforms || []), // user transforms
    ],
    directiveTransforms: Object.assign(
      {},
      directiveTransforms,
      options.directiveTransforms || {}, // user transforms
    ),
  }),
)

呼叫transform函式時傳入了兩個引數,第一個引數為當前的AST抽象語法樹,第二個引數為傳入的options,在options中我們主要看兩個屬性:nodeTransforms陣列和directiveTransforms物件。

nodeTransforms陣列由兩部分組成,分別是上一步拿到的nodeTransforms陣列,和之前在options.nodeTransforms陣列中塞進去的轉換函式。

directiveTransforms物件就不一樣了,如果上一步拿到的directiveTransforms物件和options.directiveTransforms物件擁有相同的key,那麼後者就會覆蓋前者。以我們這個例子舉例:在上一步中拿到的directiveTransforms物件中有key為model的處理v-model指令的轉換函式,但是我們在@vue/compiler-dom包中的compile函式同樣也給options.directiveTransforms物件中塞了一個key為model的處理v-model指令的轉換函式。那麼@vue/compiler-dom包中的v-model轉換函式就會覆蓋上一步中定義的v-model轉換函式,那麼@vue/compiler-core包中v-model轉換函式是不是就沒用了呢?答案是當然有用,在@vue/compiler-dom包中的v-model轉換函式會手動呼叫@vue/compiler-core包中v-model轉換函式。這樣設計的目的是對於一些指令的處理支援不同的平臺傳入不同的轉換函式,並且在這些平臺中也可以手動呼叫@vue/compiler-core包中提供的指令轉換函式,根據手動呼叫的結果再針對各自平臺進行一些特別的處理。

我們先來回憶一下前面demo中的程式碼:

<template>
  <input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const msgList = ref([
  {
    id: 1,
    value: "",
  },
  {
    id: 2,
    value: "",
  },
  {
    id: 3,
    value: "",
  },
]);
</script>

接著在debug終端中看看執行transform函式前的AST抽象語法樹是什麼樣的,如下圖:
AST

從上圖中可以看到AST抽象語法樹根節點下面只有一個children節點,這個children節點對應的就是input標籤。在input標籤上面有三個props,分別對應的是input標籤上面的v-for指令、:key屬性、v-model指令。說明在生成AST抽象語法樹的階段不會對指令進行處理,而是當做普通的屬性一樣使用正則匹配出來,然後塞到props陣列中。

既然在生成AST抽象語法樹的過程中沒有對v-modelv-for等指令進行處理,那麼又是在什麼時候處理的呢?答案是在執行transform函式的時候處理的,在transform函式中會遞迴遍歷整個AST抽象語法樹,在遍歷每個node節點時都會將nodeTransforms陣列中的所有轉換函式按照順序取出來執行一遍,在執行時將當前的node節點和上下文作為引數傳入。經過nodeTransforms陣列中全部的轉換函式處理後,vue提供的許多內建指令、語法糖、內建元件等也就被處理了,接下來只需要執行generate函式生成render函式就行了。

nodeTransforms陣列

nodeTransforms 主要是對 node節點 進行操作,可能會替換或者移動節點。每個node節點都會將nodeTransforms陣列中的轉換函式按照順序全部執行一遍,比如處理v-if指令的transformIf轉換函式就要比處理v-for指令的transformFor函式先執行。所以nodeTransforms是一個陣列,而且陣列中的轉換函式的順序還是有講究的。

在我們這個demo中input標籤上面的v-for指令是由nodeTransforms陣列中的transformFor轉換函式處理的,很簡單就可以找到transformFor轉換函式。在函式開始的地方打一個斷點,程式碼就會走到這個斷點中,在debug終端上面看看此時的node節點是什麼樣的,如下圖:
before-transformFor

從上圖中可以看到在執行transformFor轉換函式之前的node節點和上一張圖列印的node節點是一樣的。

我們在執行完transformFor轉換函式的地方打一個斷點,看看執行完transformFor轉換函式後node節點變成什麼樣了,如下圖:
after-transformFor

從上圖我們可以看到經過transformFor轉換函式處理後當前的node節點已經變成了一個新的node節點,而原來的input的node節點變成了這個節點的children子節點。新節點的source.content裡存的是v-for="item in msgList"中的msgList變數。新節點的valueAlias.content裡存的是v-for="item in msgList"中的item。我們發現input子節點的props陣列現在只有兩項了,原本的v-for指令的props經過transformFor轉換函式的處理後已經被消費掉了,所以就只有兩項了。

看到這裡你可能會有疑問,為什麼執行transform函式後會將AST抽象語法樹的結構都改變了呢?

這樣做的目的是在後續的generate函式中遞迴遍歷AST抽象語法樹時,只想進行字串拼接就可以拼成render函式。這裡涉及到模版AST抽象語法樹Javascript AST抽象語法樹的概念。

我們來回憶一下template模組中的程式碼:

<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>

template模版經過parse函式拿到AST抽象語法樹,此時的AST抽象語法樹的結構和template模版的結構是一模一樣的,所以我們稱之為模版AST抽象語法樹模版AST抽象語法樹其實就是描述template模版的結構。如下圖:
template-AST

我們再來看看生成的render函式的程式碼:

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(true), _createElementBlock(
    _Fragment,
    null,
    _renderList($setup.msgList, (item) => {
      return _withDirectives((_openBlock(), _createElementBlock("input", {
        key: item.id,
        "onUpdate:modelValue": ($event) => item.value = $event
      }, null, 8, _hoisted_1)), [
        [_vModelText, item.value]
      ]);
    }),
    128
    /* KEYED_FRAGMENT */
  );
}

很明顯模版AST抽象語法樹無法透過簡單的字串拼接就可以拼成上面的render函式,所以我們需要一個結構和上面的render函式一模一樣的Javascript AST抽象語法樹Javascript AST抽象語法樹的作用就是描述render函式的結構。如下圖:
javascript-AST

上面這個Javascript AST抽象語法樹就是執行transform函式時根據模版AST抽象語法樹生成的。有了Javascript AST抽象語法樹後再來執行generate函式時就可以只進行簡單的字串拼接,就能得到render函式了。

directiveTransforms物件

directiveTransforms物件的作用是對指令進行轉換,給node節點生成對應的props。比如給子元件上面使用了v-model指令,經過directiveTransforms物件中的transformModel轉換函式處理後,v-mode節點上面就會多兩個props屬性:modelValueonUpdate:modelValue屬性。directiveTransforms物件中的轉換函式不會每次都全部執行,而是要node節點中有對應的指令,才會執行指令的轉換函式。所以directiveTransforms是物件,而不是陣列。

那為什麼有的指令轉換函式在directiveTransforms物件中,有的又在nodeTransforms陣列中呢?

答案是在directiveTransforms物件中的指令全部都是會給node節點生成props屬性的,那些不生成props屬性的就在nodeTransforms陣列中。

很容易就可以找到@vue/compiler-dom包的transformModel函式,然後打一個斷點,讓斷點走進transformModel函式中,如下圖:
transformModel

從上面的圖中我們可以看到在@vue/compiler-dom包的transformModel函式中會呼叫@vue/compiler-core包的transformModel函式,拿到返回的baseResult物件後再一些其他操作後直接return baseResult。從左邊的call stack呼叫棧中我們可以看到transformModel函式是由一個buildProps函式呼叫的,看名字你應該猜到了buildProps函式的作用是生成props屬性的。點選Step Out將斷點跳出transformModel函式,走進buildProps函式中,可以看到buildProps函式中呼叫transformModel函式的程式碼如下圖:
buildProps

從上圖中可以看到,name變數的值為modelcontext.directiveTransforms[name]的返回值就是transformModel函式,所以執行directiveTransform(prop, node, context)其實就是在執行transformModel函式。在debug終端中可以看到返回的props2是一個陣列,裡面存的是v-model指令被處理後生成的props屬性。props屬性陣列中只有一項是onUpdate:modelValue屬性,看到這裡有的小夥伴會疑惑了v-model指令不是會生成modelValueonUpdate:modelValue兩個屬性,為什麼這裡只有一個呢?答案是隻有給自定義元件上面使用v-model指令才會生成modelValueonUpdate:modelValue兩個屬性,對於這種原生input標籤是不需要生成modelValue屬性的,因為input標籤本身是不接收名為modelValue屬性,接收的是value屬性。

其實transform函式中的內容是非常複雜的,裡面包含了vue提供的指令、filter、slot等功能的處理邏輯。transform函式的設計高明之處就在於外掛化,將處理這些功能的transform轉換函式以外掛的形式插入的,這樣邏輯就會非常清晰了。比如我想看v-model指令是如何實現的,我只需要去看對應的transformModel轉換函式就行了。又比如哪天vue需要實現一個v-xxx指令,要實現這個指令只需要增加一個transformXxx的轉換函式就行了。

generate函式

經過上一步transform函式的處理後,已經將描述模版結構的模版AST抽象語法樹轉換為了描述render函式結構的Javascript AST抽象語法樹。在前面我們已經講過了Javascript AST抽象語法樹就是描述了最終生成render函式的樣子。所以在generate函式中只需要遞迴遍歷Javascript AST抽象語法樹,透過字串拼接的方式就可以生成render函式了。

將斷點走到執行generate函式前,看看這會兒的Javascript AST抽象語法樹是什麼樣的,如下圖:
before-generate

從上面的圖中可以看到Javascript AST模版AST的區別主要有兩個:

  • node節點中多了一個codegenNode屬性,這個屬性中存了許多node節點資訊,比如codegenNode.props中就存了keyonUpdate:modelValue屬性的資訊。在generate函式中遍歷每個node節點時就會讀取這個codegenNode屬性生成render函式

  • 模版AST中根節點下面的children節點就是input標籤,但是在這裡Javascript AST中卻是根節點下面的children節點,再下面的children節點才是input標籤。多了一層節點,在前面的transform函式中我們已經講了多的這層節點是由v-for指令生成的,用於給v-for迴圈出來的多個節點當父節點。

將斷點走到generate函式執行之後,可以看到已經生成render函式啦,如下圖:
after-generate

總結

現在我們再來看看最開始講的流程圖,我想你應該已經能將整個流程串起來了。如下圖:
full-progress

將template編譯為render函式可以分為7步:

  • 執行@vue/compiler-sfc包的compileTemplate函式,裡面會呼叫同一個包的doCompileTemplate函式。這一步存在的目的是作為一個入口函式給外部呼叫。

  • 執行@vue/compiler-sfc包的doCompileTemplate函式,裡面會呼叫@vue/compiler-dom包中的compile函式。這一步存在的目的是入口函式的具體實現。

  • 執行@vue/compiler-dom包中的compile函式,裡面會對options進行了擴充套件,塞了一些處理dom的轉換函式進去。給options.nodeTransforms陣列中塞了處理style的轉換函式,和給options.directiveTransforms物件中塞了處理v-cloakv-htmlv-textv-modelv-onv-show等指令的轉換函式。然後以擴充套件後的options去呼叫@vue/compiler-core包的baseCompile函式。

  • 執行@vue/compiler-core包的baseCompile函式,在這個函式中主要分為4部分。第一部分為檢查傳入的source是不是html字串,如果是就呼叫同一個包下的baseParse函式生成模版AST抽象語法樹。否則就直接使用傳入的模版AST抽象語法樹。此時node節點中還有v-forv-model等指令,並沒有被處理掉。這裡的模版AST抽象語法樹的結構和template中的結構一模一樣,模版AST抽象語法樹是對template中的結構進行描述。

  • 第二部分為執行getBaseTransformPreset函式拿到@vue/compiler-core包中內建的nodeTransformsdirectiveTransforms轉換函式。nodeTransforms陣列中的為一堆處理node節點的轉換函式,比如處理v-on指令的transformOnce轉換函式、處理v-if指令的transformIf轉換函式。directiveTransforms物件中存的是對一些“會生成props的指令”進行轉換的函式,用於給node節點生成對應的props。比如處理v-model指令的transformModel轉換函式。

  • 第三部分為將傳入的options.nodeTransformsoptions.directiveTransforms分別和本地的nodeTransformsdirectiveTransforms進行合併得到一堆新的轉換函式。其中由於nodeTransforms是陣列,所以在合併的過程中會將options.nodeTransformsnodeTransforms中的轉換函式全部合併進去。由於directiveTransforms是物件,如果directiveTransforms物件和options.directiveTransforms物件擁有相同的key,那麼後者就會覆蓋前者。然後將合併的結果和模版AST抽象語法樹一起傳入到transform函式中執行,就可以得到轉換後的javascript AST抽象語法樹。在這一過程中v-forv-model等指令已經被轉換函式給處理了。得到的javascript AST抽象語法樹的結構和render函式的結構一模一樣,javascript AST抽象語法樹就是對render函式的結構進行描述。

  • 第四部分為由於已經拿到了和render函式的結構一模一樣的javascript AST抽象語法樹,只需要在generate函式中遍歷javascript AST抽象語法樹進行字串拼接就可以得到render函式了。

關注公眾號:前端歐陽,解鎖我更多vue乾貨文章。還可以加我微信,私信我想看哪些vue原理文章,我會根據大家的反饋進行創作。
qrcode
wxcode

相關文章