前言
我們每天寫vue3程式碼的時候都會使用到setup語法糖,那你知道為什麼setup語法糖中的頂層繫結可以在template中直接使用的呢?setup語法糖是如何編譯成setup函式的呢?本文將圍繞這些問題帶你揭開setup語法糖的神秘面紗。注:本文中使用的vue版本為3.4.19
。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
看個demo
看個簡單的demo,程式碼如下:
<template>
<h1>{{ msg }}</h1>
<h2>{{ format(msg) }}</h2>
<h3>{{ title }}</h3>
<Child />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";
import { format } from "./util.js";
const msg = ref("Hello World!");
let title;
if (msg.value) {
const innerContent = "xxx";
console.log(innerContent);
title = "111";
} else {
title = "222";
}
</script>
在上面的demo中定義了四個頂層繫結:Child
子元件、從util.js
檔案中匯入的format
方法、使用ref定義的msg
只讀常量、使用let定義的title
變數。並且在template中直接使用了這四個頂層繫結。
由於innerContent
是在if語句裡面的變數,不是<script setup>
中的頂層繫結,所以在template中是不能使用innerContent
的。
但是你有沒有想過為什麼<script setup>
中的頂層繫結就能在template中使用,而像innerContent
這種非頂層繫結就不能在template中使用呢?
我們先來看看上面的程式碼編譯後的樣子,在之前的文章中已經講過很多次如何在瀏覽器中檢視編譯後的vue檔案,這篇文章就不贅述了。編譯後的程式碼如下:
import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import { ref } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import Child from "/src/components/setupDemo2/child.vue";
import { format } from "/src/components/setupDemo2/util.js";
const _sfc_main = _defineComponent({
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const msg = ref("Hello World!");
let title;
if (msg.value) {
const innerContent = "xxx";
console.log(innerContent);
title = "111";
} else {
title = "222";
}
const __returned__ = {
msg,
get title() {
return title;
},
set title(v) {
title = v;
},
Child,
get format() {
return format;
},
};
return __returned__;
},
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
// ...省略
}
_sfc_main.render = _sfc_render;
export default _sfc_main;
從上面的程式碼中可以看到編譯後已經沒有了<script setup>
,取而代之的是一個setup函式,這也就證明了為什麼說setup是一個編譯時語法糖。
setup函式的引數有兩個,第一個引數為元件的 props
。第二個引數為Setup 上下文物件,上下文物件暴露了其他一些在 setup
中可能會用到的值,比如:expose
等。
再來看看setup函式中的內容,其實和我們的原始碼差不多,只是多了一個return。使用return會將元件中的那四個頂層繫結暴露出去,所以在template中就可以直接使用<script setup>
中的頂層繫結。
值的一提的是在return物件中title
變數和format
函式有點特別。title
、format
這兩個都是屬於訪問器屬性,其他兩個msg
、Child
屬於常見的資料屬性。
title
是一個訪問器屬性,同時擁有get
和 set
,讀取title
變數時會走進get
中,當給title
變數賦值時會走進set
中。
format
也是一個訪問器屬性,他只擁有get
,呼叫format
函式時會走進get
中。由於他沒有set
,所以不能給format
函式重新賦值。其實這個也很容易理解,因為format
函式是從util.js
檔案中import匯入的,當然不能給他重新賦值。
至於在template中是怎麼拿到setup
函式返回的物件可以看我的另外一篇文章: Vue 3 的 setup語法糖到底是什麼東西?
看到這裡有的小夥伴會有疑問了,不是還有一句import { ref } from "vue"
也是頂層繫結,為什麼裡面的ref
沒有在setup函式中使用return暴露出去呢?還有在return物件中是如何將title
、format
識別為訪問器屬性呢?
在接下來的文章中我會逐一解答這些問題。
compileScript
函式
在之前的 透過debug搞清楚.vue檔案怎麼變成.js檔案文章中已經講過了vue的script模組中的內容是由@vue/compiler-sfc
包中的compileScript
函式處理的,當然你沒看過那篇文章也不會影響這篇文章的閱讀。
首先我們需要啟動一個debug終端。這裡以vscode
舉例,開啟終端然後點選終端中的+
號旁邊的下拉箭頭,在下拉中點選Javascript Debug Terminal
就可以啟動一個debug
終端。
然後在node_modules
中找到vue/compiler-sfc
包的compileScript
函式打上斷點,compileScript
函式位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js
。接下來我們先看看簡化後的compileScript
函式原始碼。
簡化後的compileScript
函式
在debug
終端上面執行yarn dev
後在瀏覽器中開啟對應的頁面,比如:http://localhost:5173/ 。此時斷點就會走到compileScript
函式中,在我們這個場景中簡化後的compileScript
函式程式碼如下:
function compileScript(sfc, options) {
// ---- 第一部分 ----
// 根據<script setup>中的內容生成一個ctx上下文物件
// 在ctx上下文物件中擁有一些屬性和方法
const ctx = new ScriptCompileContext(sfc, options);
const { source, filename } = sfc;
// 頂層宣告的變數、函式組成的物件
const setupBindings = Object.create(null);
// script標籤中的內容開始位置和結束位置
const startOffset = ctx.startOffset;
const endOffset = ctx.endOffset;
// script setup中的內容編譯成的AST抽象語法樹
const scriptSetupAst = ctx.scriptSetupAst;
// ---- 第二部分 ----
// 遍歷<script setup>中的內容,處理裡面的import語句、頂層變數、函式、類、列舉宣告還有宏函式
for (const node of scriptSetupAst.body) {
if (node.type === "ImportDeclaration") {
// ...省略
}
}
for (const node of scriptSetupAst.body) {
if (
(node.type === "VariableDeclaration" ||
node.type === "FunctionDeclaration" ||
node.type === "ClassDeclaration" ||
node.type === "TSEnumDeclaration") &&
!node.declare
) {
// 頂層宣告的變數、函式、類、列舉宣告組成的setupBindings物件
// 給setupBindings物件賦值,{msg: 'setup-ref'}
// 頂層宣告的變數組成的setupBindings物件
walkDeclaration(
"scriptSetup",
node,
setupBindings,
vueImportAliases,
hoistStatic
);
}
}
// ---- 第三部分 ----
// 移除template中的內容和script的開始標籤
ctx.s.remove(0, startOffset);
// 移除style中的內容和script的結束標籤
ctx.s.remove(endOffset, source.length);
// ---- 第四部分 ----
// 將<script setup>中的頂層繫結的後設資料儲存到ctx.bindingMetadata物件中
// 為什麼要多此一舉儲存一個bindingMetadata物件呢?答案是setup的return的物件有時會直接返回頂層變數,有時會返回變數的get方法,有時會返回變數的get和set方法,
// 所以才需要一個bindingMetadata物件來儲存這些頂層繫結的後設資料。
for (const [key, { isType, imported, source: source2 }] of Object.entries(
ctx.userImports
)) {
if (isType) continue;
ctx.bindingMetadata[key] =
imported === "*" ||
(imported === "default" && source2.endsWith(".vue")) ||
source2 === "vue"
? "setup-const"
: "setup-maybe-ref";
}
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key];
}
// 生成setup方法的args引數;
let args = `__props`;
const destructureElements =
ctx.hasDefineExposeCall || !options.inlineTemplate
? [`expose: __expose`]
: [];
if (destructureElements.length) {
args += `, { ${destructureElements.join(", ")} }`;
}
// ---- 第五部分 ----
// 根據<script setup>中的頂層繫結生成return物件中的內容
let returned;
const allBindings = {
...setupBindings,
};
for (const key in ctx.userImports) {
// 不是引入ts中的型別並且import匯入的變數還需要在template中使用
if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {
allBindings[key] = true;
}
}
returned = `{ `;
for (const key in allBindings) {
if (
allBindings[key] === true &&
ctx.userImports[key].source !== "vue" &&
!ctx.userImports[key].source.endsWith(".vue")
) {
returned += `get ${key}() { return ${key} }, `;
} else if (ctx.bindingMetadata[key] === "setup-let") {
const setArg = key === "v" ? `_v` : `v`;
returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
} else {
returned += `${key}, `;
}
}
returned = returned.replace(/, $/, "") + ` }`;
ctx.s.appendRight(
endOffset,
`
const __returned__ = ${returned}
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
`
);
// ---- 第六部分 ----
// 生成setup函式
ctx.s.prependLeft(
startOffset,
`
${genDefaultAs} /*#__PURE__*/${ctx.helper(
`defineComponent`
)}({${def}${runtimeOptions}
${hasAwait ? `async ` : ``}setup(${args}) {
${exposeCall}`
);
ctx.s.appendRight(endOffset, `})`);
// ---- 第七部分 ----
// 插入import vue語句
if (ctx.helperImports.size > 0) {
ctx.s.prepend(
`import { ${[...ctx.helperImports]
.map((h) => `${h} as _${h}`)
.join(", ")} } from 'vue'
`
);
}
return {
// ...省略
bindings: ctx.bindingMetadata,
imports: ctx.userImports,
content: ctx.s.toString(),
};
}
首先我們來看看compileScript
函式的第一個引數sfc
物件,在之前的文章 vue檔案是如何編譯為js檔案 中我們已經講過了sfc
是一個descriptor
物件,descriptor
物件是由vue檔案編譯來的。
descriptor
物件擁有template屬性、scriptSetup屬性、style屬性,分別對應vue檔案的<template>
模組、<script setup>
模組、<style>
模組。
在我們這個場景只關注scriptSetup
屬性,sfc.scriptSetup.content
的值就是<script setup>
模組中code
程式碼字串,
sfc.source
的值就是vue
檔案中的原始碼code字串。sfc.scriptSetup.loc.start.offset
為<script setup>
中內容開始位置,sfc.scriptSetup.loc.end.offset
為<script setup>
中內容結束位置。詳情檢視下圖:
我們再來看compileScript
函式中的內容,在compileScript
函式中包含了從<script setup>
語法糖到setup函式的完整流程。乍一看可能比較難以理解,所以我將其分為七塊。
-
根據
<script setup>
中的內容生成一個ctx上下文物件。 -
遍歷
<script setup>
中的內容,處理裡面的import語句、頂層變數、頂層函式、頂層類、頂層列舉宣告等。 -
移除template和style中的內容,以及script的開始標籤和結束標籤。
-
將
<script setup>
中的頂層繫結的後設資料儲存到ctx.bindingMetadata
物件中。 -
根據
<script setup>
中的頂層繫結生成return物件。 -
生成setup函式定義
-
插入import vue語句
在接下來的文章中我將逐個分析這七塊的內容。
生成ctx上下文物件
我們來看第一塊的程式碼,如下:
// 根據<script setup>中的內容生成一個ctx上下文物件
// 在ctx上下文物件中擁有一些屬性和方法
const ctx = new ScriptCompileContext(sfc, options);
const { source, filename } = sfc;
// 頂層宣告的變數、函式組成的物件
const setupBindings = Object.create(null);
// script標籤中的內容開始位置和結束位置
const startOffset = ctx.startOffset;
const endOffset = ctx.endOffset;
// script setup中的內容編譯成的AST抽象語法樹
const scriptSetupAst = ctx.scriptSetupAst;
在這一塊的程式碼中主要做了一件事,使用ScriptCompileContext
建構函式new了一個ctx上下文物件。在之前的 為什麼defineProps宏函式不需要從vue中import匯入?文章中我們已經講過了ScriptCompileContext
建構函式里面的具體程式碼,這篇文章就不贅述了。
本文只會講用到的ScriptCompileContext
類中的startOffset
、endOffset
、scriptSetupAst
、userImports
、helperImports
、bindingMetadata
、s
等屬性。
-
startOffset
、endOffset
屬性是在ScriptCompileContext
類的constructor
建構函式中賦值的。其實就是sfc.scriptSetup.loc.start.offset
和sfc.scriptSetup.loc.end.offset
,<script setup>
中內容開始位置和<script setup>
中內容結束位置,只是將這兩個欄位塞到ctx上下文中。 -
scriptSetupAst
是在ScriptCompileContext
類的constructor
建構函式中賦值的,他是<script setup>
模組的程式碼轉換成的AST抽象語法樹。在ScriptCompileContext
類的constructor
建構函式中會呼叫@babel/parser
包的parse
函式,以<script setup>
中的code程式碼字串為引數生成AST抽象語法樹。 -
userImports
在new一個ctx上下文物件時是一個空物件,用於儲存import匯入的頂層繫結內容。 -
helperImports
同樣在new一個ctx上下文物件時是一個空物件,用於儲存需要從vue中import匯入的函式。 -
bindingMetadata
同樣在new一個ctx上下文物件時是一個空物件,用於儲存所有的import頂層繫結和變數頂層繫結的後設資料。 -
s
屬性是在ScriptCompileContext
類的constructor
建構函式中賦值的,以vue
檔案中的原始碼code字串為引數new
了一個MagicString
物件賦值給s
屬性。
magic-string
是由svelte的作者寫的一個庫,用於處理字串的JavaScript
庫。它可以讓你在字串中進行插入、刪除、替換等操作,並且能夠生成準確的sourcemap
。
MagicString
物件中擁有toString
、remove
、prependLeft
、appendRight
等方法。s.toString
用於生成返回的字串,我們來舉幾個例子看看這幾個方法你就明白了。
s.remove( start, end )
用於刪除從開始到結束的字串:
const s = new MagicString('hello word');
s.remove(0, 6);
s.toString(); // 'word'
s.prependLeft( index, content )
用於在指定index
的前面插入字串:
const s = new MagicString('hello word');
s.prependLeft(5, 'xx');
s.toString(); // 'helloxx word'
s.appendRight( index, content )
用於在指定index
的後面插入字串:
const s = new MagicString('hello word');
s.appendRight(5, 'xx');
s.toString(); // 'helloxx word'
除了上面說的那幾個屬性,在這裡定義了一個setupBindings
變數。初始值是一個空物件,用於儲存頂層宣告的變數、函式等。
遍歷<script setup>
body中的內容
將斷點走到第二部分,程式碼如下:
for (const node of scriptSetupAst.body) {
if (node.type === "ImportDeclaration") {
// ...省略
}
}
for (const node of scriptSetupAst.body) {
if (
(node.type === "VariableDeclaration" ||
node.type === "FunctionDeclaration" ||
node.type === "ClassDeclaration" ||
node.type === "TSEnumDeclaration") &&
!node.declare
) {
// 頂層宣告的變數、函式、類、列舉宣告組成的setupBindings物件
// 給setupBindings物件賦值,{msg: 'setup-ref'}
// 頂層宣告的變數組成的setupBindings物件
walkDeclaration(
"scriptSetup",
node,
setupBindings,
vueImportAliases,
hoistStatic
);
}
}
在這一部分的程式碼中使用for迴圈遍歷了兩次scriptSetupAst.body
,scriptSetupAst.body
為script中的程式碼對應的AST抽象語法樹中body的內容,如下圖:
從上圖中可以看到scriptSetupAst.body
陣列有6項,分別對應的是script模組中的6塊程式碼。
第一個for迴圈中使用if判斷node.type === "ImportDeclaration"
,也就是判斷是不是import語句。如果是import語句,那麼import的內容肯定是頂層繫結,需要將import匯入的內容儲存到ctx.userImports
物件中。注:後面會專門寫一篇文章來講如何收集所有的import匯入。
透過這個for迴圈已經將所有的import匯入收集到了ctx.userImports
物件中了,在debug終端看看此時的ctx.userImports
,如下圖:
從上圖中可以看到在ctx.userImports
中收集了三個import匯入,分別是Child
元件、format
函式、ref
函式。
在裡面有幾個欄位需要注意,isUsedInTemplate
表示當前import匯入的東西是不是在template中使用,如果為true那麼就需要將這個import匯入塞到return物件中。
isType
表示當前import匯入的是不是type型別,因為在ts中是可以使用import匯入type型別,很明顯type型別也不需要塞到return物件中。
我們再來看第二個for迴圈,同樣也是遍歷scriptSetupAst.body
。如果當前是變數定義、函式定義、類定義、ts列舉定義,這四種型別都屬於頂層繫結(除了import匯入以外就只有這四種頂層繫結了)。需要呼叫walkDeclaration
函式將這四種頂層繫結收集到setupBindings
物件中。
從前面的scriptSetupAst.body
圖中可以看到if模組的type為IfStatement
,明顯不屬於上面的這四種型別,所以不會執行walkDeclaration
函式將裡面的innerContent
變數收集起來後面再塞到return物件中。這也就解釋了為什麼非頂層繫結不能在template中直接使用。
我們在debug終端來看看執行完第二個for迴圈後setupBindings
物件是什麼樣的,如下圖:
從上圖中可以看到在setupBindings
物件中收集msg
和title
這兩個頂層變數。其中的setup-ref
表示當前變數是一個ref定義的變數,setup-let
表示當前變數是一個let定義的變數。
移除template模組和style模組
接著將斷點走到第三部分,程式碼如下:
ctx.s.remove(0, startOffset);
ctx.s.remove(endOffset, source.length);
這塊程式碼很簡單,startOffset
為<script setup>
中的內容開始位置,endOffset
為<script setup>
中的內容結束位置,ctx.s.remove
方法為刪除字串。
所以ctx.s.remove(0, startOffset)
的作用是:移除template中的內容和script的開始標籤。
ctx.s.remove(endOffset, source.length)
的作用是:移除style中的內容和script的結束標籤。
我們在debug終端看看執行這兩個remove
方法之前的code程式碼字串是什麼樣的,如下圖:
從上圖中可以看到此時的code程式碼字串和我們原始碼差不多,唯一的區別就是那幾個import匯入已經被提取到script標籤外面去了(這個是在前面第一個for迴圈處理import匯入的時候處理的)。
將斷點走到執行完這兩個remove方法之後,在debug終端看看此時的code程式碼字串,如下圖:
從上圖中可以看到執行這兩個remove
方法後template模組、style模組(雖然本文demo中沒有寫style模組)、script開始標籤、script結束標籤都已經被刪除了。唯一剩下的就是script模組中的內容,還有之前提出去的那幾個import匯入。
將頂層繫結的後設資料儲存到ctx.bindingMetadata
接著將斷點走到第四部分,程式碼如下:
for (const [key, { isType, imported, source: source2 }] of Object.entries(
ctx.userImports
)) {
if (isType) continue;
ctx.bindingMetadata[key] =
imported === "*" ||
(imported === "default" && source2.endsWith(".vue")) ||
source2 === "vue"
? "setup-const"
: "setup-maybe-ref";
}
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key];
}
// 生成setup函式的args引數;
let args = `__props`;
const destructureElements =
ctx.hasDefineExposeCall || !options.inlineTemplate
? [`expose: __expose`]
: [];
if (destructureElements.length) {
args += `, { ${destructureElements.join(", ")} }`;
}
上面的程式碼主要分為三塊,第一塊為for迴圈遍歷前面收集到的ctx.userImports
物件。這個物件裡面收集的是所有的import匯入,將所有import匯入塞到ctx.bindingMetadata
物件中。
第二塊也是for迴圈遍歷前面收集的setupBindings
物件,這個物件裡面收集的是頂層宣告的變數、函式、類、列舉,同樣的將這些頂層繫結塞到ctx.bindingMetadata
物件中。
為什麼要多此一舉儲存一個ctx.bindingMetadata
物件呢?
答案是setup的return的物件有時會直接返回頂層變數(比如demo中的msg
常量)。有時只會返回變數的訪問器屬性 get(比如demo中的format
函式)。有時會返回變數的訪問器屬性 get和set(比如demo中的title
變數)。所以才需要一個ctx.bindingMetadata
物件來儲存這些頂層繫結的後設資料。
將斷點走到執行完這兩個for迴圈的地方,在debug終端來看看此時收集的ctx.bindingMetadata
物件是什麼樣的,如下圖:
最後一塊程式碼也很簡單進行字串拼接生成setup函式的引數,第一個引數為元件的props、第二個引數為expose
方法組成的物件。如下圖:
生成return物件
接著將斷點走到第五部分,程式碼如下:
let returned;
const allBindings = {
...setupBindings,
};
for (const key in ctx.userImports) {
// 不是引入ts中的型別並且import匯入的變數還需要在template中使用
if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {
allBindings[key] = true;
}
}
returned = `{ `;
for (const key in allBindings) {
if (
allBindings[key] === true &&
ctx.userImports[key].source !== "vue" &&
!ctx.userImports[key].source.endsWith(".vue")
) {
returned += `get ${key}() { return ${key} }, `;
} else if (ctx.bindingMetadata[key] === "setup-let") {
const setArg = key === "v" ? `_v` : `v`;
returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
} else {
returned += `${key}, `;
}
}
returned = returned.replace(/, $/, "") + ` }`;
ctx.s.appendRight(
endOffset,
`
const __returned__ = ${returned}
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
`
);
這部分的程式碼看著很多,其實邏輯也非常清晰,我也將其分為三塊。
在第一塊中首先使用擴充套件運算子...setupBindings
將setupBindings
物件中的屬性合併到allBindings
物件中,因為setupBindings
物件中存的頂層宣告的變數、函式、類、列舉都需要被return出去。
然後遍歷ctx.userImports
物件,前面講過了ctx.userImports
物件中存的是所有的import匯入(包括從vue中import匯入ref函式)。在迴圈裡面執行了if判斷!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate
,這個判斷的意思是如果當前import匯入的不是ts的type型別並且import匯入的內容在template模版中使用了。才會去執行allBindings[key] = true
,執行後就會將滿足條件的import匯入塞到allBindings
物件中。
後面生成setup函式的return物件就是透過遍歷這個allBindings
物件實現的。這也就解釋了為什麼從vue中import匯入的ref函式也是頂層繫結,為什麼他沒有被setup函式返回。因為只有在template中使用的import匯入頂層繫結才會被setup函式返回。
將斷點走到遍歷ctx.userImports
物件之後,在debug終端來看看此時的allBindings
物件是什麼樣的,如下圖:
從上圖中可以看到此時的allBindings
物件中存了四個需要return的頂層繫結。
接著就是執行for迴圈遍歷allBindings
物件生成return物件的字串,這迴圈中有三個if判斷條件。我們先來看第一個,程式碼如下:
if (
allBindings[key] === true &&
ctx.userImports[key].source !== "vue" &&
!ctx.userImports[key].source.endsWith(".vue")
) {
returned += `get ${key}() { return ${key} }, `;
}
if條件判斷是:如果當前import匯入不是從vue中,並且也不是import匯入一個vue元件。那麼就給return一個只擁有get的訪問器屬性,對應我們demo中的就是import { format } from "./util.js"
中的format
函式。
我們再來看第二個else if判斷,程式碼如下:
else if (ctx.bindingMetadata[key] === "setup-let") {
const setArg = key === "v" ? `_v` : `v`;
returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
}
這個else if條件判斷是:如果當前頂層繫結是一個let定義的變數。那麼就給return一個同時擁有get和set的訪問器屬性,對應我們demo中的就是let title"
變數。
最後就是else,程式碼如下:
else {
returned += `${key}, `;
}
這個else中就是普通的資料屬性了,對應我們demo中的就是msg
變數和Child
元件。
將斷點走到生成return物件之後,在debug終端來看看此時生成的return物件是什麼樣的,如下圖:
從上圖中可以看到此時已經生成了return物件啦。
前面我們只生成了return物件,但是還沒將其插入到要生成的code字串中,所以需要執行ctx.s.appendRight
方法在末尾插入return的程式碼。
將斷點走到執行完ctx.s.appendRight
方法後,在debug終端來看看此時的code程式碼字串是什麼樣的,如下圖:
從上圖中可以看到此時的code程式碼字串中多了一塊return的程式碼。
生成setup函式定義
接著將斷點走到第六部分,程式碼如下:
ctx.s.prependLeft(
startOffset,
`
${genDefaultAs} /*#__PURE__*/${ctx.helper(
`defineComponent`
)}({${def}${runtimeOptions}
${hasAwait ? `async ` : ``}setup(${args}) {
${exposeCall}`
);
ctx.s.appendRight(endOffset, `})`);
這部分的程式碼很簡單,呼叫ctx.s.prependLeft
方法從左邊插入一串程式碼。插入的這串程式碼就是簡單的字串拼接,我們在debug終端來看看要插入的程式碼是什麼樣的,如下圖:
是不是覺得上面這塊需要插入的程式碼看著很熟悉,他就是編譯後的_sfc_main
物件除去setup函式內容的部分。將斷點走到ctx.s.appendRight
方法執行之後,再來看看此時的code程式碼字串是什麼樣的,如下圖:
從上圖中可以看到此時的setup函式基本已經生成完了。
插入import vue語句
上一步生成的code程式碼字串其實還有一個問題,在程式碼中使用了_defineComponent
函式,但是沒有從任何地方去import匯入。
第七塊的程式碼就會生成缺少的import匯入,程式碼如下:
if (ctx.helperImports.size > 0) {
ctx.s.prepend(
`import { ${[...ctx.helperImports]
.map((h) => `${h} as _${h}`)
.join(", ")} } from 'vue'
`
);
}
將斷點走到ctx.s.prepend
函式執行後,再來看看此時的code程式碼字串,如下圖:
從上圖中可以看到已經生成了完整的setup函式啦。
總結
整個流程圖如下:
-
遍歷
<script setup>
中的程式碼將所有的import匯入收集到ctx.userImports
物件中。 -
遍歷
<script setup>
中的程式碼將所有的頂層變數、函式、類、列舉收集到setupBindings
物件中。 -
呼叫
ctx.s.remove
方法移除template、style模組以及script開始標籤和結束標籤。 -
遍歷前面收集的
ctx.userImports
和setupBindings
物件,將所有的頂層繫結後設資料儲存到bindingMetadata
物件中。 -
遍歷前面收集的
ctx.userImports
和setupBindings
物件,生成return物件中的內容。在這一步的時候會將沒有在template中使用的import匯入給過濾掉,這也就解釋了為什麼從vue中匯入的ref函式不包含在return物件中。 -
呼叫
ctx.s.prependLeft
方法生成setup的函式定義。 -
呼叫
ctx.s.prepend
方法生成完整的setup函式。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會