在我之前的文章中講了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
函數這一過程的流程圖,讓你對整個流程有個大概的印象,後面的內容看著就不費勁了。如下圖:
從上面的流程圖可以看到整個流程可以分為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-for
、v-model
等指令。這裡的模版AST抽象語法樹
結構和template模塊中的代碼結構是一模一樣的,所以說模版AST抽象語法樹
就是對template模塊中的結構進行描述。 -
第二部分為執行
getBaseTransformPreset
函數拿到@vue/compiler-core
包中內置的nodeTransforms
和directiveTransforms
轉換函數。 -
第三部分為將傳入的
options.nodeTransforms
、options.directiveTransforms
分別和本地的nodeTransforms
、directiveTransforms
進行合併得到一堆新的轉換函數,和模版AST抽象語法樹
一起傳入到transform
函數中執行,就會得到轉換後的javascript AST抽象語法樹
。在這一過程中v-for
、v-model
等指令已經被轉換函數給處理了。得到的javascript AST抽象語法樹
的結構和將要生成的render
函數的結構是一模一樣的,所以說javascript AST抽象語法樹
就是對render
函數的結構進行描述。 -
第四部分為由於已經拿到了和render函數的結構一模一樣的
javascript AST抽象語法樹
,只需要在generate
函數中遍歷javascript AST抽象語法樹
進行字元串拼接就可以得到render
函數了。
關註公眾號:前端歐陽
,解鎖我更多vue
乾貨文章。還可以加我微信,私信我想看哪些vue
原理文章,我會根據大家的反饋進行創作。
@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
終端。
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終端中看看compiler
、source
、inAST
這三個變數的值是長什麼樣的。如下圖:
從上圖中我們可以看到此時的compiler
變數的值為undefined
,source
變數的值為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-dom
的compile
函數處理後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-dom
的compile
函數處理後directiveTransforms
對象中增加了處理v-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的transform
轉換函數。很明顯我們這個demo中input
標簽上面的v-model
指令就是由這裡的transformModel
轉換函數處理。
你發現了沒,不管是nodeTransforms
數組還是directiveTransforms
對象,增加的transform
轉換函數都是處理dom相關的。經過@vue/compiler-dom
的compile
函數處理後,再調用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>
模塊。如下圖:
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-once
、v-if
、v-memo
、v-for
等指令的轉換函數。很明顯我們這個demo中input
標簽上面的v-for
指令就是由這裡的transformFor
轉換函數處理。
同理由getBaseTransformPreset
函數的返回值解構出來的directiveTransforms
變數是一個對象,對象中包含處理v-on
、v-bind
、v-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抽象語法樹根節點下麵只有一個children節點,這個children節點對應的就是input標簽。在input標簽上面有三個props,分別對應的是input標簽上面的v-for
指令、:key
屬性、v-model
指令。說明在生成AST抽象語法樹的階段不會對指令進行處理,而是當做普通的屬性一樣使用正則匹配出來,然後塞到props數組中。
既然在生成AST抽象語法樹的過程中沒有對v-model
、v-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
節點是什麼樣的,如下圖:
從上圖中可以看到在執行transformFor
轉換函數之前的node節點和上一張圖列印的node節點是一樣的。
我們在執行完transformFor
轉換函數的地方打一個斷點,看看執行完transformFor
轉換函數後node節點變成什麼樣了,如下圖:
從上圖我們可以看到經過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
模版的結構。如下圖:
我們再來看看生成的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抽象語法樹
就是執行transform
函數時根據模版AST抽象語法樹
生成的。有了Javascript AST抽象語法樹
後再來執行generate
函數時就可以只進行簡單的字元串拼接,就能得到render
函數了。
directiveTransforms
對象
directiveTransforms
對象的作用是對指令進行轉換,給node
節點生成對應的props
。比如給子組件上面使用了v-model
指令,經過directiveTransforms
對象中的transformModel
轉換函數處理後,v-mode
節點上面就會多兩個props屬性:modelValue
和onUpdate:modelValue
屬性。directiveTransforms
對象中的轉換函數不會每次都全部執行,而是要node節點中有對應的指令,才會執行指令的轉換函數。所以directiveTransforms
是對象,而不是數組。
那為什麼有的指令轉換函數在directiveTransforms
對象中,有的又在nodeTransforms
數組中呢?
答案是在directiveTransforms
對象中的指令全部都是會給node節點生成props屬性的,那些不生成props屬性的就在nodeTransforms
數組中。
很容易就可以找到@vue/compiler-dom
包的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
函數的代碼如下圖:
從上圖中可以看到,name
變數的值為model
。context.directiveTransforms[name]
的返回值就是transformModel
函數,所以執行directiveTransform(prop, node, context)
其實就是在執行transformModel
函數。在debug終端中可以看到返回的props2
是一個數組,裡面存的是v-model
指令被處理後生成的props屬性。props屬性數組中只有一項是onUpdate:modelValue
屬性,看到這裡有的小伙伴會疑惑了v-model
指令不是會生成modelValue
和onUpdate:modelValue
兩個屬性,為什麼這裡只有一個呢?答案是只有給自定義組件上面使用v-model
指令才會生成modelValue
和onUpdate: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抽象語法樹
是什麼樣的,如下圖:
從上面的圖中可以看到Javascript AST
和模版AST
的區別主要有兩個:
-
node節點中多了一個
codegenNode
屬性,這個屬性中存了許多node節點信息,比如codegenNode.props
中就存了key
和onUpdate:modelValue
屬性的信息。在generate
函數中遍歷每個node節點時就會讀取這個codegenNode
屬性生成render
函數 -
模版AST
中根節點下麵的children節點就是input標簽,但是在這裡Javascript AST
中卻是根節點下麵的children節點,再下麵的children節點才是input標簽。多了一層節點,在前面的transform
函數中我們已經講了多的這層節點是由v-for
指令生成的,用於給v-for
迴圈出來的多個節點當父節點。
將斷點走到generate
函數執行之後,可以看到已經生成render
函數啦,如下圖:
總結
現在我們再來看看最開始講的流程圖,我想你應該已經能將整個流程串起來了。如下圖:
將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-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的轉換函數。然後以擴展後的options
去調用@vue/compiler-core
包的baseCompile
函數。 -
執行
@vue/compiler-core
包的baseCompile
函數,在這個函數中主要分為4部分。第一部分為檢查傳入的source是不是html字元串,如果是就調用同一個包下的baseParse
函數生成模版AST抽象語法樹
。否則就直接使用傳入的模版AST抽象語法樹
。此時node節點中還有v-for
、v-model
等指令,並沒有被處理掉。這裡的模版AST抽象語法樹
的結構和template中的結構一模一樣,模版AST抽象語法樹
是對template中的結構進行描述。 -
第二部分為執行
getBaseTransformPreset
函數拿到@vue/compiler-core
包中內置的nodeTransforms
和directiveTransforms
轉換函數。nodeTransforms
數組中的為一堆處理node節點的轉換函數,比如處理v-on
指令的transformOnce
轉換函數、處理v-if
指令的transformIf
轉換函數。directiveTransforms
對象中存的是對一些“會生成props的指令”進行轉換的函數,用於給node
節點生成對應的props
。比如處理v-model
指令的transformModel
轉換函數。 -
第三部分為將傳入的
options.nodeTransforms
、options.directiveTransforms
分別和本地的nodeTransforms
、directiveTransforms
進行合併得到一堆新的轉換函數。其中由於nodeTransforms
是數組,所以在合併的過程中會將options.nodeTransforms
和nodeTransforms
中的轉換函數全部合併進去。由於directiveTransforms
是對象,如果directiveTransforms
對象和options.directiveTransforms
對象擁有相同的key,那麼後者就會覆蓋前者。然後將合併的結果和模版AST抽象語法樹
一起傳入到transform
函數中執行,就可以得到轉換後的javascript AST抽象語法樹
。在這一過程中v-for
、v-model
等指令已經被轉換函數給處理了。得到的javascript AST抽象語法樹
的結構和render函數的結構一模一樣,javascript AST抽象語法樹
就是對render
函數的結構進行描述。 -
第四部分為由於已經拿到了和render函數的結構一模一樣的
javascript AST抽象語法樹
,只需要在generate
函數中遍歷javascript AST抽象語法樹
進行字元串拼接就可以得到render
函數了。
關註公眾號:前端歐陽
,解鎖我更多vue
乾貨文章。還可以加我微信,私信我想看哪些vue
原理文章,我會根據大家的反饋進行創作。