前言
在之前的 透過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/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
原理文章,我會根據大家的反饋進行創作。