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

来源:https://www.cnblogs.com/heavenYJJ/p/18128196
-Advertisement-
Play Games

在我之前的文章中講了vue文件是如何編譯成js文件,今天這篇文章接著來講講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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在我寫虛擬記憶體時遇到一些問題,jalr t0就是可以的,而call main則沒辦法成功 而且這裡預設main是偏移前的地址,怪哉!!! 讓我們看看jalr 和call(偽指令!!!) 然後在鏈接後看看main的地址,主要看程式中main是不是偏移 懸著的心也死了!!! 查看一下ucore也有類似的 ...
  • linux伺服器部署了一個tcp服務,需要進行併發壓力測試 看看伺服器能支持多少個tcp長連接 預計會有50w個連接 需要設置linux 不然是無法支持這麼多連接的 如果達到這個值是無法建立新連接的 報錯信息一般為 too many open files 1 , fs.file-max linux系 ...
  • setreuid(-1,foo) 為何有時更新 saved set-user-id 有時不更新?man setreuid 中關於 SUID 變更的說明是否正確?如此設計有何考慮?本文通過閱讀 linux 源碼為你揭秘 ...
  • 痞子衡嵌入式半月刊: 第 97 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • @目錄前言簡介一、準備工作1.1 創建寫入腳本1.2 設置執行許可權1.3 添加定時任務1.4 配置生效二、Tomcat日誌 按每天分割2.1 創建一個 sh文件2.2 設置執行許可權2.3 設置crontab指令,指定每日定時任務2.4 配置生效總結 前言 請各大網友尊重本人原創知識分享,謹記本人博客 ...
  • Android Compose 入門,深入底層源碼分析 我是跟著AS官網學習的,但是官方的教程寫的不是很詳細.官網鏈接 首先創建一個Compose項目,目錄結構是這樣: ui -> theme -> -> Color.kt -> -> Theme.kt -> -> Type.kt MainActiv ...
  • 一、Popup Popup組件通常用於在屏幕上彈出一個對話框或者浮動視窗。這個組件通常和其他組件一起用於用戶界面的交互和反饋。 Popup組件可以包含任何類型的組件或內容,比如文本、按鈕、輸入框、圖片等等。在打開和關閉Popup時,可以在代碼中設置不同的動畫效果來增強用戶體驗。 Popup組件的 ...
  • 一、Video 視頻組件是用於應用程式中嵌入視頻的一種方法。它可以讓用戶在網站上觀看視頻並與其進行交互。通常,視頻組件將一個視頻文件嵌入應用程式中,並提供一組控制項,這些控制項允許用戶播放、暫停、跳過、音量調整和全屏等。通過使用視頻組件,開發者可以更容易地將視頻嵌入應用程式,使其更易於管理和控制。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...