有點東西,Template可以直接使用Setup語法糖中的變量原來是因為這個
前言
我們每天寫vue3代碼的時候都會使用到setup語法糖,那你知道為什么setup語法糖中的頂層綁定可以在template中直接使用的呢?setup語法糖是如何編譯成setup函數的呢?本文將圍繞這些問題帶你揭開setup語法糖的神秘面紗。注:本文中使用的vue版本為3.4.19。
看個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變量。初始值是一個空對象,用于存儲頂層聲明的變量、函數等。
遍歷
將斷點走到第二部分,代碼如下:
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函數。