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

前言

在之前的 通過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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章