前言
在之前的 通過debug搞清楚.vue文件怎么變成.js文件 文章中我們講過了vue文件是如何編譯成js文件,通過那篇文章我們知道了,template編譯為render函數(shù)底層就是調(diào)用了@vue/compiler-sfc
包暴露出來的compileTemplate
函數(shù)。由于文章篇幅有限,我們沒有去深入探索compileTemplate
函數(shù)是如何將template模塊編譯為render
函數(shù),在這篇文章中我們來了解一下。
@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相關(guān)的邏輯都在這里面。 -
@vue/compiler-core
:從名字你也能看出來這個包是vue編譯部分的核心,提供了通用的編譯邏輯,不管是瀏覽器端還是服務(wù)端編譯最終都會走到這個包里面來。
先來看個流程圖
先來看一下我畫的template模塊編譯為render
函數(shù)這一過程的流程圖,讓你對整個流程有個大概的印象,后面的內(nèi)容看著就不費勁了。如下圖:
從上面的流程圖可以看到整個流程可以分為7步:
-
執(zhí)行
@vue/compiler-sfc
包的compileTemplate
函數(shù),里面會調(diào)用同一個包的doCompileTemplate
函數(shù)。 -
執(zhí)行
@vue/compiler-sfc
包的doCompileTemplate
函數(shù),里面會調(diào)用@vue/compiler-dom
包中的compile
函數(shù)。 -
執(zhí)行
@vue/compiler-dom
包中的compile
函數(shù),里面會對options
進(jìn)行了擴(kuò)展,塞了一些處理dom的轉(zhuǎn)換函數(shù)進(jìn)去。分別塞到了options.nodeTransforms
數(shù)組和options.directiveTransforms
對象中。然后以擴(kuò)展后的options
去調(diào)用@vue/compiler-core
包的baseCompile
函數(shù)。 -
執(zhí)行
@vue/compiler-core
包的baseCompile
函數(shù),在這個函數(shù)中主要分為4部分。第一部分為檢查傳入的source是不是html字符串,如果是就調(diào)用同一個包下的baseParse
函數(shù)生成模版AST抽象語法樹
。否則就直接使用傳入的模版AST抽象語法樹
。此時node節(jié)點中還有v-for
、v-model
等指令。這里的模版AST抽象語法樹
結(jié)構(gòu)和template模塊中的代碼結(jié)構(gòu)是一模一樣的,所以說模版AST抽象語法樹
就是對template模塊中的結(jié)構(gòu)進(jìn)行描述。 -
第二部分為執(zhí)行
getBaseTransformPreset
函數(shù)拿到@vue/compiler-core
包中內(nèi)置的nodeTransforms
和directiveTransforms
轉(zhuǎn)換函數(shù)。 -
第三部分為將傳入的
options.nodeTransforms
、options.directiveTransforms
分別和本地的nodeTransforms
、directiveTransforms
進(jìn)行合并得到一堆新的轉(zhuǎn)換函數(shù),和模版AST抽象語法樹
一起傳入到transform
函數(shù)中執(zhí)行,就會得到轉(zhuǎn)換后的javascript AST抽象語法樹
。在這一過程中v-for
、v-model
等指令已經(jīng)被轉(zhuǎn)換函數(shù)給處理了。得到的javascript AST抽象語法樹
的結(jié)構(gòu)和將要生成的render
函數(shù)的結(jié)構(gòu)是一模一樣的,所以說javascript AST抽象語法樹
就是對render
函數(shù)的結(jié)構(gòu)進(jìn)行描述。 -
第四部分為由于已經(jīng)拿到了和render函數(shù)的結(jié)構(gòu)一模一樣的
javascript AST抽象語法樹
,只需要在generate
函數(shù)中遍歷javascript AST抽象語法樹
進(jìn)行字符串拼接就可以得到render
函數(shù)了。文章來源:http://www.zghlxwxcb.cn/news/detail-847744.html
關(guān)注公眾號:前端歐陽
,解鎖我更多vue
干貨文章。還可以加我微信,私信我想看哪些vue
原理文章,我會根據(jù)大家的反饋進(jìn)行創(chuàng)作。
@vue/compiler-sfc包的compileTemplate函數(shù)
還是同樣的套路,我們通過debug一個簡單的demo來搞清楚compileTemplate
函數(shù)是如何將template編譯成render函數(shù)的。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文件 文章中我們已經(jīng)知道了在使用vite的情況下template編譯為render函數(shù)是在node端完成的。所以我們需要啟動一個debug
終端,才可以在node端打斷點。這里以vscode舉例,首先我們需要打開終端,然后點擊終端中的+
號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal
就可以啟動一個debug
終端。
compileTemplate
函數(shù)在node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js
文件中,找到compileTemplate
函數(shù)打上斷點,然后在debug
終端中執(zhí)行yarn dev
(這里是以vite
舉例)。在瀏覽器中訪問 http://localhost:5173/,此時斷點就會走到compileTemplate
函數(shù)中了。在我們這個場景中compileTemplate
函數(shù)簡化后的代碼非常簡單,代碼如下:
function compileTemplate(options) {
return doCompileTemplate(options);
}
@vue/compiler-sfc包的doCompileTemplate函數(shù)
我們接著將斷點走進(jìn)doCompileTemplate
函數(shù)中,看看里面的代碼是什么樣的,簡化后的代碼如下:
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
函數(shù)中代碼同樣也很簡單,我們在debug終端中看看compiler
、source
、inAST
這三個變量的值是長什么樣的。如下圖:
從上圖中我們可以看到此時的compiler
變量的值為undefined
,source
變量的值為template模塊中的代碼,inAST
的值為由template模塊編譯而來的AST抽象語法樹。不是說好的要經(jīng)過parse
函數(shù)處理后才會得到AST抽象語法樹,為什么這里就已經(jīng)有了AST抽象語法樹?不要著急接著向下看,后面我會解釋。
由于這里的compiler
變量的值為undefined
,所以compiler
會被賦值為CompilerDOM
。而CompilerDOM
就是@vue/compiler-dom
包中暴露的所有內(nèi)容。執(zhí)行compiler.compile
函數(shù),就是執(zhí)行@vue/compiler-dom
包中的compile
函數(shù)。compile
函數(shù)接收的第一個參數(shù)為inAST || source
,從這里我們知道第一個參數(shù)既可能是AST抽象語法樹,也有可能是template模塊中的html代碼字符串。compile
函數(shù)的返回值對象中的code
字段就是編譯好的render
函數(shù),然后return出去。
@vue/compiler-dom包中的compile函數(shù)
我們接著將斷點走進(jìn)@vue/compiler-dom
包中的compile
函數(shù),發(fā)現(xiàn)代碼同樣也很簡單,簡化后的代碼如下:
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
函數(shù)也不是具體實現(xiàn)的地方,在這里調(diào)用的是@vue/compiler-core
包的baseCompile
函數(shù)??吹竭@里你可能會有疑問,為什么不在上一步的doCompileTemplate
函數(shù)中直接調(diào)用@vue/compiler-core
包的baseCompile
函數(shù),而是要從@vue/compiler-dom
包中繞一圈再來調(diào)用呢baseCompile
函數(shù)呢?
答案是baseCompile
函數(shù)是一個處于@vue/compiler-core
包中的API,而@vue/compiler-core
可以運行在各種 JavaScript 環(huán)境下,比如瀏覽器端、服務(wù)端等各個平臺。baseCompile
函數(shù)接收這些平臺專有的一些options,而我們這里的demo是瀏覽器平臺。所以才需要從@vue/compiler-dom
包中繞一圈去調(diào)用@vue/compiler-core
包中的baseCompile
函數(shù)傳入一些瀏覽器中特有的options。在上面的代碼中我們看到使用DOMNodeTransforms
數(shù)組對options
中的nodeTransforms
屬性進(jìn)行了擴(kuò)展,使用DOMDirectiveTransforms
對象對options
中的directiveTransforms
屬性進(jìn)行了擴(kuò)展。
我們先來看看DOMNodeTransforms
數(shù)組:
const DOMNodeTransforms = [
transformStyle
];
options
對象中的nodeTransforms
屬性是一個數(shù)組,里面包含了許多transform
轉(zhuǎn)換函數(shù)用于處理AST抽象語法樹。經(jīng)過@vue/compiler-dom
的compile
函數(shù)處理后nodeTransforms
數(shù)組中多了一個處理style的transformStyle
函數(shù)。這里的transformStyle
是一個轉(zhuǎn)換函數(shù)用于處理dom
上面的style,比如style="color: red"
。
我們再來看看DOMDirectiveTransforms
對象:
const DOMDirectiveTransforms = {
cloak: compilerCore.noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow
};
options
對象中的directiveTransforms
屬性是一個對象,經(jīng)過@vue/compiler-dom
的compile
函數(shù)處理后directiveTransforms
對象中增加了處理v-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的transform
轉(zhuǎn)換函數(shù)。很明顯我們這個demo中input
標(biāo)簽上面的v-model
指令就是由這里的transformModel
轉(zhuǎn)換函數(shù)處理。
你發(fā)現(xiàn)了沒,不管是nodeTransforms
數(shù)組還是directiveTransforms
對象,增加的transform
轉(zhuǎn)換函數(shù)都是處理dom相關(guān)的。經(jīng)過@vue/compiler-dom
的compile
函數(shù)處理后,再調(diào)用baseCompile
函數(shù)就有了處理dom相關(guān)的轉(zhuǎn)換函數(shù)了。
@vue/compiler-core包的baseCompile函數(shù)
繼續(xù)將斷點走進(jìn)vue/compiler-core
包的baseCompile
函數(shù),簡化后的baseCompile
函數(shù)代碼如下:
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
函數(shù)接收的參數(shù),第一個參數(shù)為source
,類型為string | RootNode
。這句話的意思是接收的source
變量可能是html字符串,也有可能是html字符串編譯后的AST抽象語法樹。再來看看第二個參數(shù)options
,我們這里只關(guān)注options.nodeTransforms
數(shù)組屬性和options.directiveTransforms
對象屬性,這兩個里面都是存了一堆轉(zhuǎn)換函數(shù),區(qū)別就是一個是數(shù)組,一個是對象。
我們再來看看返回值類型CodegenResult
,定義如下:
interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}
從類型中我們可以看到返回值對象中的code
屬性就是編譯好的render
函數(shù),而這個返回值就是最后調(diào)用generate
函數(shù)返回的。
明白了baseCompile
函數(shù)接收的參數(shù)和返回值,我們再來看函數(shù)內(nèi)的代碼。主要分為四塊內(nèi)容:
-
拿到由html字符串轉(zhuǎn)換成的AST抽象語法樹。
-
拿到由一堆轉(zhuǎn)換函數(shù)組成的
nodeTransforms
數(shù)組,和拿到由一堆轉(zhuǎn)換函數(shù)組成的directiveTransforms
對象。 -
執(zhí)行
transform
函數(shù),使用合并后的nodeTransforms
中的所有轉(zhuǎn)換函數(shù)處理AST抽象語法樹中的所有node節(jié)點,使用合并后的directiveTransforms
中的轉(zhuǎn)換函數(shù)對會生成props的指令進(jìn)行處理,得到處理后的javascript AST抽象語法樹
。 -
調(diào)用
generate
函數(shù)根據(jù)上一步處理后的javascript AST抽象語法樹
進(jìn)行字符串拼接,拼成render
函數(shù)。
獲取AST抽象語法樹
我們先來看第一塊的內(nèi)容,代碼如下:
const ast = isString(source) ? baseParse(source, options) : source
如果傳入的source
是html字符串,那就調(diào)用baseParse
函數(shù)根據(jù)html字符串生成對應(yīng)的AST抽象語法樹,如果傳入的就是AST抽象語法樹那么就直接賦值給ast
變量。為什么這里有這兩種情況呢?
原因是baseCompile
函數(shù)可以被直接調(diào)用,也可以像我們這樣由vite的@vitejs/plugin-vue
包發(fā)起,經(jīng)過層層調(diào)用后最終執(zhí)行baseCompile
函數(shù)。在我們這個場景中,在前面我們就知道了走進(jìn)compileTemplate
函數(shù)之前就已經(jīng)有了編譯后的AST抽象語法樹,所以這里不會再調(diào)用baseParse
函數(shù)去生成AST抽象語法樹了。那么又是什么時候生成的AST抽象語法樹呢?
在之前的 通過debug搞清楚.vue文件怎么變成.js文件 文章中我們講了調(diào)用createDescriptor
函數(shù)會將vue
代碼字符串轉(zhuǎn)換為descriptor
對象,descriptor
對象中擁有template
屬性、scriptSetup
屬性、styles
屬性,分別對應(yīng)vue文件中的template
模塊、<script setup>
模塊、<style>
模塊。如下圖:createDescriptor
函數(shù)在生成template
屬性的時候底層同樣也會調(diào)用@vue/compiler-core
包的baseParse
函數(shù),將template模塊中的html字符串編譯為AST抽象語法樹。
所以在我們這個場景中走到baseCompile
函數(shù)時就已經(jīng)有了AST抽象語法樹了,其實底層都調(diào)用的是@vue/compiler-core
包的baseParse
函數(shù)。
獲取轉(zhuǎn)換函數(shù)
接著將斷點走到第二塊內(nèi)容處,代碼如下:
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
從上面的代碼可以看到getBaseTransformPreset
函數(shù)的返回值是一個數(shù)組,對返回的數(shù)組進(jìn)行解構(gòu),數(shù)組的第一項賦值給nodeTransforms
變量,數(shù)組的第二項賦值給directiveTransforms
變量。
將斷點走進(jìn)getBaseTransformPreset
函數(shù),代碼如下:
function getBaseTransformPreset() {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
transformFilter,
trackVForSlotScopes,
transformExpression
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
];
}
從上面的代碼中不難看出由getBaseTransformPreset
函數(shù)的返回值解構(gòu)出來的nodeTransforms
變量是一個數(shù)組,數(shù)組中包含一堆transform轉(zhuǎn)換函數(shù),比如處理v-once
、v-if
、v-memo
、v-for
等指令的轉(zhuǎn)換函數(shù)。很明顯我們這個demo中input
標(biāo)簽上面的v-for
指令就是由這里的transformFor
轉(zhuǎn)換函數(shù)處理。
同理由getBaseTransformPreset
函數(shù)的返回值解構(gòu)出來的directiveTransforms
變量是一個對象,對象中包含處理v-on
、v-bind
、v-model
指令的轉(zhuǎn)換函數(shù)。
經(jīng)過這一步的處理我們就拿到了由一系列轉(zhuǎn)換函數(shù)組成的nodeTransforms
數(shù)組,和由一系列轉(zhuǎn)換函數(shù)組成的directiveTransforms
對象??吹竭@里我想你可能有一些疑問,為什么nodeTransforms
是數(shù)組,directiveTransforms
卻是對象呢?為什么有的指令轉(zhuǎn)換轉(zhuǎn)換函數(shù)是在nodeTransforms
數(shù)組中,有的卻是在directiveTransforms
對象中呢?別著急,我們下面會講。
transform函數(shù)
接著將斷點走到第三塊內(nèi)容,transform
函數(shù)處,代碼如下:
transform(
ast,
Object.assign({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: Object.assign(
{},
directiveTransforms,
options.directiveTransforms || {}, // user transforms
),
}),
)
調(diào)用transform
函數(shù)時傳入了兩個參數(shù),第一個參數(shù)為當(dāng)前的AST抽象語法樹,第二個參數(shù)為傳入的options
,在options
中我們主要看兩個屬性:nodeTransforms
數(shù)組和directiveTransforms
對象。
nodeTransforms
數(shù)組由兩部分組成,分別是上一步拿到的nodeTransforms
數(shù)組,和之前在options.nodeTransforms
數(shù)組中塞進(jìn)去的轉(zhuǎn)換函數(shù)。
directiveTransforms
對象就不一樣了,如果上一步拿到的directiveTransforms
對象和options.directiveTransforms
對象擁有相同的key,那么后者就會覆蓋前者。以我們這個例子舉例:在上一步中拿到的directiveTransforms
對象中有key為model
的處理v-model
指令的轉(zhuǎn)換函數(shù),但是我們在@vue/compiler-dom
包中的compile
函數(shù)同樣也給options.directiveTransforms
對象中塞了一個key為model
的處理v-model
指令的轉(zhuǎn)換函數(shù)。那么@vue/compiler-dom
包中的v-model
轉(zhuǎn)換函數(shù)就會覆蓋上一步中定義的v-model
轉(zhuǎn)換函數(shù),那么@vue/compiler-core
包中v-model
轉(zhuǎn)換函數(shù)是不是就沒用了呢?答案是當(dāng)然有用,在@vue/compiler-dom
包中的v-model
轉(zhuǎn)換函數(shù)會手動調(diào)用@vue/compiler-core
包中v-model
轉(zhuǎn)換函數(shù)。這樣設(shè)計的目的是對于一些指令的處理支持不同的平臺傳入不同的轉(zhuǎn)換函數(shù),并且在這些平臺中也可以手動調(diào)用@vue/compiler-core
包中提供的指令轉(zhuǎn)換函數(shù),根據(jù)手動調(diào)用的結(jié)果再針對各自平臺進(jìn)行一些特別的處理。
我們先來回憶一下前面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終端中看看執(zhí)行transform
函數(shù)前的AST抽象語法樹是什么樣的,如下圖:
從上圖中可以看到AST抽象語法樹根節(jié)點下面只有一個children節(jié)點,這個children節(jié)點對應(yīng)的就是input標(biāo)簽。在input標(biāo)簽上面有三個props,分別對應(yīng)的是input標(biāo)簽上面的v-for
指令、:key
屬性、v-model
指令。說明在生成AST抽象語法樹的階段不會對指令進(jìn)行處理,而是當(dāng)做普通的屬性一樣使用正則匹配出來,然后塞到props數(shù)組中。
既然在生成AST抽象語法樹的過程中沒有對v-model
、v-for
等指令進(jìn)行處理,那么又是在什么時候處理的呢?答案是在執(zhí)行transform
函數(shù)的時候處理的,在transform
函數(shù)中會遞歸遍歷整個AST抽象語法樹,在遍歷每個node節(jié)點時都會將nodeTransforms
數(shù)組中的所有轉(zhuǎn)換函數(shù)按照順序取出來執(zhí)行一遍,在執(zhí)行時將當(dāng)前的node節(jié)點和上下文作為參數(shù)傳入。經(jīng)過nodeTransforms
數(shù)組中全部的轉(zhuǎn)換函數(shù)處理后,vue提供的許多內(nèi)置指令、語法糖、內(nèi)置組件等也就被處理了,接下來只需要執(zhí)行generate
函數(shù)生成render
函數(shù)就行了。
nodeTransforms數(shù)組
nodeTransforms
主要是對 node節(jié)點 進(jìn)行操作,可能會替換或者移動節(jié)點。每個node節(jié)點都會將nodeTransforms
數(shù)組中的轉(zhuǎn)換函數(shù)按照順序全部執(zhí)行一遍,比如處理v-if
指令的transformIf
轉(zhuǎn)換函數(shù)就要比處理v-for
指令的transformFor
函數(shù)先執(zhí)行。所以nodeTransforms
是一個數(shù)組,而且數(shù)組中的轉(zhuǎn)換函數(shù)的順序還是有講究的。
在我們這個demo中input標(biāo)簽上面的v-for
指令是由nodeTransforms
數(shù)組中的transformFor
轉(zhuǎn)換函數(shù)處理的,很簡單就可以找到transformFor
轉(zhuǎn)換函數(shù)。在函數(shù)開始的地方打一個斷點,代碼就會走到這個斷點中,在debug終端上面看看此時的node
節(jié)點是什么樣的,如下圖:
從上圖中可以看到在執(zhí)行transformFor
轉(zhuǎn)換函數(shù)之前的node節(jié)點和上一張圖打印的node節(jié)點是一樣的。
我們在執(zhí)行完transformFor
轉(zhuǎn)換函數(shù)的地方打一個斷點,看看執(zhí)行完transformFor
轉(zhuǎn)換函數(shù)后node節(jié)點變成什么樣了,如下圖:
從上圖我們可以看到經(jīng)過transformFor
轉(zhuǎn)換函數(shù)處理后當(dāng)前的node節(jié)點已經(jīng)變成了一個新的node節(jié)點,而原來的input的node節(jié)點變成了這個節(jié)點的children子節(jié)點。新節(jié)點的source.content
里存的是v-for="item in msgList"
中的msgList
變量。新節(jié)點的valueAlias.content
里存的是v-for="item in msgList"
中的item
。我們發(fā)現(xiàn)input子節(jié)點的props數(shù)組現(xiàn)在只有兩項了,原本的v-for
指令的props經(jīng)過transformFor
轉(zhuǎn)換函數(shù)的處理后已經(jīng)被消費掉了,所以就只有兩項了。
看到這里你可能會有疑問,為什么執(zhí)行transform
函數(shù)后會將AST抽象語法樹的結(jié)構(gòu)都改變了呢?
這樣做的目的是在后續(xù)的generate
函數(shù)中遞歸遍歷AST抽象語法樹時,只想進(jìn)行字符串拼接就可以拼成render函數(shù)。這里涉及到模版AST抽象語法樹
和Javascript AST抽象語法樹
的概念。
我們來回憶一下template模塊中的代碼:
<template>
<input v-for="item in msgList" :key="item.id" v-model="item.value" />
</template>
template模版經(jīng)過parse
函數(shù)拿到AST抽象語法樹,此時的AST抽象語法樹的結(jié)構(gòu)和template模版的結(jié)構(gòu)是一模一樣的,所以我們稱之為模版AST抽象語法樹
。模版AST抽象語法樹
其實就是描述template
模版的結(jié)構(gòu)。如下圖:
我們再來看看生成的render
函數(shù)的代碼:
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
函數(shù),所以我們需要一個結(jié)構(gòu)和上面的render函數(shù)一模一樣的Javascript AST抽象語法樹
,Javascript AST抽象語法樹
的作用就是描述render
函數(shù)的結(jié)構(gòu)。如下圖:
上面這個Javascript AST抽象語法樹
就是執(zhí)行transform
函數(shù)時根據(jù)模版AST抽象語法樹
生成的。有了Javascript AST抽象語法樹
后再來執(zhí)行generate
函數(shù)時就可以只進(jìn)行簡單的字符串拼接,就能得到render
函數(shù)了。
directiveTransforms對象
directiveTransforms
對象的作用是對指令進(jìn)行轉(zhuǎn)換,給node
節(jié)點生成對應(yīng)的props
。比如給子組件上面使用了v-model
指令,經(jīng)過directiveTransforms
對象中的transformModel
轉(zhuǎn)換函數(shù)處理后,v-mode
節(jié)點上面就會多兩個props屬性:modelValue
和onUpdate:modelValue
屬性。directiveTransforms
對象中的轉(zhuǎn)換函數(shù)不會每次都全部執(zhí)行,而是要node節(jié)點中有對應(yīng)的指令,才會執(zhí)行指令的轉(zhuǎn)換函數(shù)。所以directiveTransforms
是對象,而不是數(shù)組。
那為什么有的指令轉(zhuǎn)換函數(shù)在directiveTransforms
對象中,有的又在nodeTransforms
數(shù)組中呢?
答案是在directiveTransforms
對象中的指令全部都是會給node節(jié)點生成props屬性的,那些不生成props屬性的就在nodeTransforms
數(shù)組中。
很容易就可以找到@vue/compiler-dom
包的transformModel
函數(shù),然后打一個斷點,讓斷點走進(jìn)transformModel
函數(shù)中,如下圖:
從上面的圖中我們可以看到在@vue/compiler-dom
包的transformModel
函數(shù)中會調(diào)用@vue/compiler-core
包的transformModel
函數(shù),拿到返回的baseResult
對象后再一些其他操作后直接return baseResult
。從左邊的call stack調(diào)用棧中我們可以看到transformModel
函數(shù)是由一個buildProps
函數(shù)調(diào)用的,看名字你應(yīng)該猜到了buildProps
函數(shù)的作用是生成props屬性的。點擊Step Out將斷點跳出transformModel
函數(shù),走進(jìn)buildProps
函數(shù)中,可以看到buildProps
函數(shù)中調(diào)用transformModel
函數(shù)的代碼如下圖:
從上圖中可以看到,name
變量的值為model
。context.directiveTransforms[name]
的返回值就是transformModel
函數(shù),所以執(zhí)行directiveTransform(prop, node, context)
其實就是在執(zhí)行transformModel
函數(shù)。在debug終端中可以看到返回的props2
是一個數(shù)組,里面存的是v-model
指令被處理后生成的props屬性。props屬性數(shù)組中只有一項是onUpdate:modelValue
屬性,看到這里有的小伙伴會疑惑了v-model
指令不是會生成modelValue
和onUpdate:modelValue
兩個屬性,為什么這里只有一個呢?答案是只有給自定義組件上面使用v-model
指令才會生成modelValue
和onUpdate:modelValue
兩個屬性,對于這種原生input標(biāo)簽是不需要生成modelValue
屬性的,因為input標(biāo)簽本身是不接收名為modelValue
屬性,接收的是value屬性。
其實transform
函數(shù)中的內(nèi)容是非常復(fù)雜的,里面包含了vue提供的指令、filter、slot等功能的處理邏輯。transform
函數(shù)的設(shè)計高明之處就在于插件化,將處理這些功能的transform轉(zhuǎn)換函數(shù)以插件的形式插入的,這樣邏輯就會非常清晰了。比如我想看v-model
指令是如何實現(xiàn)的,我只需要去看對應(yīng)的transformModel
轉(zhuǎn)換函數(shù)就行了。又比如哪天vue需要實現(xiàn)一個v-xxx
指令,要實現(xiàn)這個指令只需要增加一個transformXxx
的轉(zhuǎn)換函數(shù)就行了。
generate函數(shù)
經(jīng)過上一步transform
函數(shù)的處理后,已經(jīng)將描述模版結(jié)構(gòu)的模版AST抽象語法樹
轉(zhuǎn)換為了描述render
函數(shù)結(jié)構(gòu)的Javascript AST抽象語法樹
。在前面我們已經(jīng)講過了Javascript AST抽象語法樹
就是描述了最終生成render
函數(shù)的樣子。所以在generate
函數(shù)中只需要遞歸遍歷Javascript AST抽象語法樹
,通過字符串拼接的方式就可以生成render
函數(shù)了。
將斷點走到執(zhí)行generate
函數(shù)前,看看這會兒的Javascript AST抽象語法樹
是什么樣的,如下圖:
從上面的圖中可以看到Javascript AST
和模版AST
的區(qū)別主要有兩個:
-
node節(jié)點中多了一個
codegenNode
屬性,這個屬性中存了許多node節(jié)點信息,比如codegenNode.props
中就存了key
和onUpdate:modelValue
屬性的信息。在generate
函數(shù)中遍歷每個node節(jié)點時就會讀取這個codegenNode
屬性生成render
函數(shù) -
模版AST
中根節(jié)點下面的children節(jié)點就是input標(biāo)簽,但是在這里Javascript AST
中卻是根節(jié)點下面的children節(jié)點,再下面的children節(jié)點才是input標(biāo)簽。多了一層節(jié)點,在前面的transform
函數(shù)中我們已經(jīng)講了多的這層節(jié)點是由v-for
指令生成的,用于給v-for
循環(huán)出來的多個節(jié)點當(dāng)父節(jié)點。
將斷點走到generate
函數(shù)執(zhí)行之后,可以看到已經(jīng)生成render
函數(shù)啦,如下圖:
總結(jié)
現(xiàn)在我們再來看看最開始講的流程圖,我想你應(yīng)該已經(jīng)能將整個流程串起來了。如下圖:
將template編譯為render函數(shù)可以分為7步:
-
執(zhí)行
@vue/compiler-sfc
包的compileTemplate
函數(shù),里面會調(diào)用同一個包的doCompileTemplate
函數(shù)。這一步存在的目的是作為一個入口函數(shù)給外部調(diào)用。 -
執(zhí)行
@vue/compiler-sfc
包的doCompileTemplate
函數(shù),里面會調(diào)用@vue/compiler-dom
包中的compile
函數(shù)。這一步存在的目的是入口函數(shù)的具體實現(xiàn)。 -
執(zhí)行
@vue/compiler-dom
包中的compile
函數(shù),里面會對options
進(jìn)行了擴(kuò)展,塞了一些處理dom的轉(zhuǎn)換函數(shù)進(jìn)去。給options.nodeTransforms
數(shù)組中塞了處理style的轉(zhuǎn)換函數(shù),和給options.directiveTransforms
對象中塞了處理v-cloak
、v-html
、v-text
、v-model
、v-on
、v-show
等指令的轉(zhuǎn)換函數(shù)。然后以擴(kuò)展后的options
去調(diào)用@vue/compiler-core
包的baseCompile
函數(shù)。 -
執(zhí)行
@vue/compiler-core
包的baseCompile
函數(shù),在這個函數(shù)中主要分為4部分。第一部分為檢查傳入的source是不是html字符串,如果是就調(diào)用同一個包下的baseParse
函數(shù)生成模版AST抽象語法樹
。否則就直接使用傳入的模版AST抽象語法樹
。此時node節(jié)點中還有v-for
、v-model
等指令,并沒有被處理掉。這里的模版AST抽象語法樹
的結(jié)構(gòu)和template中的結(jié)構(gòu)一模一樣,模版AST抽象語法樹
是對template中的結(jié)構(gòu)進(jìn)行描述。 -
第二部分為執(zhí)行
getBaseTransformPreset
函數(shù)拿到@vue/compiler-core
包中內(nèi)置的nodeTransforms
和directiveTransforms
轉(zhuǎn)換函數(shù)。nodeTransforms
數(shù)組中的為一堆處理node節(jié)點的轉(zhuǎn)換函數(shù),比如處理v-on
指令的transformOnce
轉(zhuǎn)換函數(shù)、處理v-if
指令的transformIf
轉(zhuǎn)換函數(shù)。directiveTransforms
對象中存的是對一些“會生成props的指令”進(jìn)行轉(zhuǎn)換的函數(shù),用于給node
節(jié)點生成對應(yīng)的props
。比如處理v-model
指令的transformModel
轉(zhuǎn)換函數(shù)。 -
第三部分為將傳入的
options.nodeTransforms
、options.directiveTransforms
分別和本地的nodeTransforms
、directiveTransforms
進(jìn)行合并得到一堆新的轉(zhuǎn)換函數(shù)。其中由于nodeTransforms
是數(shù)組,所以在合并的過程中會將options.nodeTransforms
和nodeTransforms
中的轉(zhuǎn)換函數(shù)全部合并進(jìn)去。由于directiveTransforms
是對象,如果directiveTransforms
對象和options.directiveTransforms
對象擁有相同的key,那么后者就會覆蓋前者。然后將合并的結(jié)果和模版AST抽象語法樹
一起傳入到transform
函數(shù)中執(zhí)行,就可以得到轉(zhuǎn)換后的javascript AST抽象語法樹
。在這一過程中v-for
、v-model
等指令已經(jīng)被轉(zhuǎn)換函數(shù)給處理了。得到的javascript AST抽象語法樹
的結(jié)構(gòu)和render函數(shù)的結(jié)構(gòu)一模一樣,javascript AST抽象語法樹
就是對render
函數(shù)的結(jié)構(gòu)進(jìn)行描述。 -
第四部分為由于已經(jīng)拿到了和render函數(shù)的結(jié)構(gòu)一模一樣的
javascript AST抽象語法樹
,只需要在generate
函數(shù)中遍歷javascript AST抽象語法樹
進(jìn)行字符串拼接就可以得到render
函數(shù)了。
關(guān)注公眾號:前端歐陽
,解鎖我更多vue
干貨文章。還可以加我微信,私信我想看哪些vue
原理文章,我會根據(jù)大家的反饋進(jìn)行創(chuàng)作。文章來源地址http://www.zghlxwxcb.cn/news/detail-847744.html
到了這里,關(guān)于看不懂來打我,vue3如何將template編譯成render函數(shù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!