defineProps宏函數(shù)不需要從vue中import導(dǎo)入的原因解析
前言
我們每天寫vue
代碼時(shí)都在用defineProps
,但是你有沒有思考過下面這些問題。為什么defineProps
不需要import
導(dǎo)入?為什么不能在非setup
頂層使用defineProps
?defineProps
是如何將聲明的 props
自動暴露給模板?
舉幾個例子
我們來看幾個例子,分別對應(yīng)上面的幾個問題。
先來看一個正常的例子,common-child.vue
文件代碼如下:
<template> <div>content is {{ content }}</div> </template> <script setup lang="ts"> defineProps({ content: String, }); </script>
我們看到在這個正常的例子中沒有從任何地方import
導(dǎo)入defineProps
,直接就可以使用了,并且在template
中渲染了props
中的content
。
我們再來看一個在非setup
頂層使用defineProps
的例子,if-child.vue
文件代碼如下:
<template> <div>content is {{ content }}</div> </template> <script setup lang="ts"> import { ref } from "vue"; const count = ref(10); if (count.value) { defineProps({ content: String, }); } </script>
代碼跑起來直接就報(bào)錯了,提示defineProps is not defined
通過debug搞清楚上面幾個問題
在我的上一篇文章 vue文件是如何編譯為js文件 中已經(jīng)帶你搞清楚了vue
文件中的<script>
模塊是如何編譯成瀏覽器可直接運(yùn)行的js
代碼,其實(shí)底層就是依靠vue/compiler-sfc
包的compileScript
函數(shù)。
當(dāng)然如果你還沒看過我的上一篇文章也不影響這篇文章閱讀,這里我會簡單說一下。當(dāng)我們import
一個vue
文件時(shí)會觸發(fā)@vitejs/plugin-vue包的transform
鉤子函數(shù),在這個函數(shù)中會調(diào)用一個transformMain
函數(shù)。transformMain
函數(shù)中會調(diào)用genScriptCode
、genTemplateCode
、genStyleCode
,分別對應(yīng)的作用是將vue
文件中的<script>
模塊編譯為瀏覽器可直接運(yùn)行的js
代碼、將<template>
模塊編譯為render
函數(shù)、將<style>
模塊編譯為導(dǎo)入css
文件的import
語句。genScriptCode
函數(shù)底層調(diào)用的就是vue/compiler-sfc
包的compileScript
函數(shù)。
一樣的套路,首先我們在vscode的打開一個debug
終端。
然后在node_modules
中找到vue/compiler-sfc
包的compileScript
函數(shù)打上斷點(diǎn),compileScript
函數(shù)位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js
。在debug
終端上面執(zhí)行yarn dev
后在瀏覽器中打開對應(yīng)的頁面,比如:http://localhost:5173/ 。此時(shí)斷點(diǎn)就會走到compileScript
函數(shù)中,我們在debug
中先來看看compileScript
函數(shù)的第一個入?yún)?code>sfc。sfc.filename
的值為當(dāng)前編譯的vue
文件路徑。由于每編譯一個vue
文件都要走到這個debug中,現(xiàn)在我們只想debug
看看common-child.vue
文件,所以為了方便我們在compileScript
中加了下面這樣一段代碼,并且去掉了在compileScript
函數(shù)中加的斷點(diǎn),這樣就只有編譯common-child.vue
文件時(shí)會走進(jìn)斷點(diǎn)。
compileScript
函數(shù)
我們再來回憶一下common-child.vue
文件中的script
模塊代碼如下:
<script setup lang="ts"> defineProps({ content: String, }); </script>
我們接著來看compileScript
函數(shù)的入?yún)?code>sfc,在上一篇文章 vue文件是如何編譯為js文件 中我們已經(jīng)講過了sfc
是一個descriptor
對象,descriptor
對象是由vue
文件編譯來的。descriptor
對象擁有template
屬性、scriptSetup
屬性、style
屬性,分別對應(yīng)vue
文件的<template>
模塊、<script setup>
模塊、<style>
模塊。在我們這個場景只關(guān)注scriptSetup
屬性,sfc.scriptSetup.content
的值就是<script setup>
模塊中code
代碼字符串,sfc.source
的值就是vue
文件中的源代碼code字符串。詳情查看下圖:
compileScript
函數(shù)內(nèi)包含了編譯script
模塊的所有的邏輯,代碼很復(fù)雜,光是源代碼就接近1000行。這篇文章我們不會去通讀compileScript
函數(shù)的所有功能,只會講處理defineProps
相關(guān)的代碼。下面這個是我簡化后的代碼:
function compileScript(sfc, options) { const ctx = new ScriptCompileContext(sfc, options); const startOffset = ctx.startOffset; const endOffset = ctx.endOffset; const scriptSetupAst = ctx.scriptSetupAst; for (const node of scriptSetupAst.body) { if (node.type === "ExpressionStatement") { const expr = node.expression; if (processDefineProps(ctx, expr)) { ctx.s.remove(node.start + startOffset, node.end + startOffset); } } if (node.type === "VariableDeclaration" && !node.declare || node.type.endsWith("Statement")) { // .... } } ctx.s.remove(0, startOffset); ctx.s.remove(endOffset, source.length); let runtimeOptions = ``; const propsDecl = genRuntimeProps(ctx); if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`; const def = (defaultExport ? `\n ...${normalScriptDefaultVar},` : ``) + (definedOptions ? `\n ...${definedOptions},` : ""); ctx.s.prependLeft( startOffset, `\n${genDefaultAs} /*#__PURE__*/${ctx.helper( `defineComponent` )}({${def}${runtimeOptions}\n ${ hasAwait ? `async ` : `` }setup(${args}) {\n${exposeCall}` ); ctx.s.appendRight(endOffset, `})`); return { //.... content: ctx.s.toString(), }; }
在compileScript
函數(shù)中首先調(diào)用ScriptCompileContext
類生成一個ctx
上下文對象,然后遍歷vue
文件的<script setup>
模塊生成的AST抽象語法樹
。如果節(jié)點(diǎn)類型為ExpressionStatement
表達(dá)式語句,那么就執(zhí)行processDefineProps
函數(shù),判斷當(dāng)前表達(dá)式語句是否是調(diào)用defineProps
函數(shù)。如果是那么就刪除掉defineProps
調(diào)用代碼,并且將調(diào)用defineProps
函數(shù)時(shí)傳入的參數(shù)對應(yīng)的node
節(jié)點(diǎn)信息存到ctx
上下文中。然后從參數(shù)node
節(jié)點(diǎn)信息中拿到調(diào)用defineProps
宏函數(shù)時(shí)傳入的props
參數(shù)的開始位置和結(jié)束位置。再使用slice
方法并且傳入開始位置和結(jié)束位置,從<script setup>
模塊的代碼字符串中截取到props
定義的字符串。然后將截取到的props
定義的字符串拼接到vue
組件對象的字符串中,最后再將編譯后的setup
函數(shù)代碼字符串拼接到vue
組件對象的字符串中。
ScriptCompileContext
類
ScriptCompileContext
類中我們主要關(guān)注這幾個屬性:startOffset
、endOffset
、scriptSetupAst
、s
。先來看看他的constructor
,下面是我簡化后的代碼。
import MagicString from 'magic-string' class ScriptCompileContext { source = this.descriptor.source s = new MagicString(this.source) startOffset = this.descriptor.scriptSetup?.loc.start.offset endOffset = this.descriptor.scriptSetup?.loc.end.offset constructor(descriptor, options) { this.s = new MagicString(this.source); this.scriptSetupAst = descriptor.scriptSetup && parse(descriptor.scriptSetup.content, this.startOffset); } }
在前面我們已經(jīng)講過了descriptor.scriptSetup
對象就是由vue
文件中的<script setup>
模塊編譯而來,startOffset
和endOffset
分別就是descriptor.scriptSetup?.loc.start.offset
和descriptor.scriptSetup?.loc.end.offset
,對應(yīng)的是<script setup>
模塊在vue
文件中的開始位置和結(jié)束位置。
descriptor.source
的值就是vue
文件中的源代碼code字符串,這里以descriptor.source
為參數(shù)new
了一個MagicString
對象。magic-string
是由svelte的作者寫的一個庫,用于處理字符串的JavaScript
庫。它可以讓你在字符串中進(jìn)行插入、刪除、替換等操作,并且能夠生成準(zhǔn)確的sourcemap
。MagicString
對象中擁有toString
、remove
、prependLeft
、appendRight
等方法。s.toString
用于生成返回的字符串,我們來舉幾個例子看看這幾個方法你就明白了。
s.remove( start, end )
用于刪除從開始到結(jié)束的字符串:
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'
我們接著看constructor
中的scriptSetupAst
屬性是由一個parse
函數(shù)的返回值賦值,parse(descriptor.scriptSetup.content, this.startOffset)
,parse
函數(shù)的代碼如下:
import { parse as babelParse } from '@babel/parser' function parse(input: string, offset: number): Program { try { return babelParse(input, { plugins, sourceType: 'module', }).program } catch (e: any) { } }
我們在前面已經(jīng)講過了descriptor.scriptSetup.content
的值就是vue
文件中的<script setup>
模塊的代碼code
字符串,parse
函數(shù)中調(diào)用了babel
提供的parser
函數(shù),將vue
文件中的<script setup>
模塊的代碼code
字符串轉(zhuǎn)換成AST抽象語法樹
。
現(xiàn)在我們再來看compileScript
函數(shù)中的這幾行代碼你理解起來就沒什么難度了,這里的scriptSetupAst
變量就是由vue
文件中的<script setup>
模塊的代碼轉(zhuǎn)換成的AST抽象語法樹
。
const ctx = new ScriptCompileContext(sfc, options); const startOffset = ctx.startOffset; const endOffset = ctx.endOffset; const scriptSetupAst = ctx.scriptSetupAst;
流程圖如下:
processDefineProps
函數(shù)
我們接著將斷點(diǎn)走到for
循環(huán)開始處,代碼如下:
for (const node of scriptSetupAst.body) { if (node.type === "ExpressionStatement") { const expr = node.expression; if (processDefineProps(ctx, expr)) { ctx.s.remove(node.start + startOffset, node.end + startOffset); } } }
遍歷AST抽象語法樹
,如果當(dāng)前節(jié)點(diǎn)類型為ExpressionStatement
表達(dá)式語句,并且processDefineProps
函數(shù)執(zhí)行結(jié)果為true
就調(diào)用ctx.s.remove
方法。這會兒斷點(diǎn)還在for
循環(huán)開始處,在控制臺執(zhí)行ctx.s.toString()
看看當(dāng)前的code
代碼字符串。
從圖上可以看見此時(shí)toString
的執(zhí)行結(jié)果還是和之前的common-child.vue
源代碼是一樣的,并且很明顯我們的defineProps
是一個表達(dá)式語句,所以會執(zhí)行processDefineProps
函數(shù)。我們將斷點(diǎn)走到調(diào)用processDefineProps
的地方,看到簡化過的processDefineProps
函數(shù)代碼如下:
const DEFINE_PROPS = "defineProps"; function processDefineProps(ctx, node, declId) { if (!isCallOf(node, DEFINE_PROPS)) { return processWithDefaults(ctx, node, declId); } ctx.propsRuntimeDecl = node.arguments[0]; return true; }
在processDefineProps
函數(shù)中首先執(zhí)行了isCallOf
函數(shù),第一個參數(shù)傳的是當(dāng)前的AST語法樹
中的node
節(jié)點(diǎn),第二個參數(shù)傳的是"defineProps"
字符串。從isCallOf
的名字中我們就可以猜出他的作用是判斷當(dāng)前的node
節(jié)點(diǎn)的類型是不是在調(diào)用defineProps
函數(shù),isCallOf
的代碼如下:
export function isCallOf(node, test) { return !!( node && test && node.type === "CallExpression" && node.callee.type === "Identifier" && (typeof test === "string" ? node.callee.name === test : test(node.callee.name)) ); }
isCallOf
函數(shù)接收兩個參數(shù),第一個參數(shù)node
是當(dāng)前的node
節(jié)點(diǎn),第二個參數(shù)test
是要判斷的函數(shù)名稱,在我們這里是寫死的"defineProps"
字符串。我們在debug console
中將node.type
、node.callee.type
、node.callee.name
的值打印出來看看。
從圖上看到node.type
、node.callee.type
、node.callee.name
的值后,可以證明我們的猜測是正確的這里isCallOf
的作用是判斷當(dāng)前的node
節(jié)點(diǎn)的類型是不是在調(diào)用defineProps
函數(shù)。我們這里的node
節(jié)點(diǎn)確實(shí)是在調(diào)用defineProps
函數(shù),所以isCallOf
的執(zhí)行結(jié)果為true
,在processDefineProps
函數(shù)中是對isCallOf
函數(shù)的執(zhí)行結(jié)果取反。也就是!isCallOf(node, DEFINE_PROPS)
的執(zhí)行結(jié)果為false
,所以不會走到return processWithDefaults(ctx, node, declId);
。
我們接著來看processDefineProps
函數(shù):
function processDefineProps(ctx, node, declId) { if (!isCallOf(node, DEFINE_PROPS)) { return processWithDefaults(ctx, node, declId); } ctx.propsRuntimeDecl = node.arguments[0]; return true; }
如果當(dāng)前節(jié)點(diǎn)確實(shí)是在執(zhí)行defineProps
函數(shù),那么就會執(zhí)行ctx.propsRuntimeDecl = node.arguments[0];
。將當(dāng)前node
節(jié)點(diǎn)的第一個參數(shù)賦值給ctx
上下文對象的propsRuntimeDecl
屬性,這里的第一個參數(shù)其實(shí)就是調(diào)用defineProps
函數(shù)時(shí)給傳入的第一個參數(shù)。為什么寫死成取arguments[0]
呢?是因?yàn)?code>defineProps函數(shù)只接收一個參數(shù),傳入的參數(shù)可以是一個對象或者數(shù)組。比如:
const props = defineProps({ foo: String }) const props = defineProps(['foo', 'bar'])
記住這個在ctx
上下文上面塞的propsRuntimeDecl
屬性,后面生成運(yùn)行時(shí)的props
就是根據(jù)propsRuntimeDecl
屬性生成的。
至此我們已經(jīng)了解到了processDefineProps
中主要做了兩件事:判斷當(dāng)前執(zhí)行的表達(dá)式語句是否是defineProps
函數(shù),如果是那么將解析出來的props
屬性的信息塞的ctx
上下文的propsRuntimeDecl
屬性中。
我們這會兒來看compileScript
函數(shù)中的processDefineProps
代碼你就能很容易理解了:
for (const node of scriptSetupAst.body) { if (node.type === "ExpressionStatement") { const expr = node.expression; if (processDefineProps(ctx, expr)) { ctx.s.remove(node.start + startOffset, node.end + startOffset); } } }
遍歷AST語法樹
,如果當(dāng)前節(jié)點(diǎn)類型是ExpressionStatement
表達(dá)式語句,再執(zhí)行processDefineProps
判斷當(dāng)前node
節(jié)點(diǎn)是否是執(zhí)行的defineProps
函數(shù)。如果是defineProps
函數(shù)就調(diào)用ctx.s.remove
方法將調(diào)用defineProps
函數(shù)的代碼從源代碼中刪除掉。此時(shí)我們在debug console
中執(zhí)行ctx.s.toString()
,看到我們的code
代碼字符串中已經(jīng)沒有了defineProps
了:
現(xiàn)在我們能夠回答第一個問題了:
為什么defineProps
不需要import
導(dǎo)入?
因?yàn)樵诰幾g過程中如果當(dāng)前AST抽象語法樹
的節(jié)點(diǎn)類型是ExpressionStatement
表達(dá)式語句,并且調(diào)用的函數(shù)是defineProps
,那么就調(diào)用remove
方法將調(diào)用defineProps
函數(shù)的代碼給移除掉。既然defineProps
語句已經(jīng)被移除了,自然也就不需要import
導(dǎo)入了defineProps
了。
genRuntimeProps
函數(shù)
接著在compileScript
函數(shù)中執(zhí)行了兩條remove
代碼:
ctx.s.remove(0, startOffset); ctx.s.remove(endOffset, source.length);
這里的startOffset
表示script
標(biāo)簽中第一個代碼開始的位置, 所以ctx.s.remove(0, startOffset);
的意思是刪除掉包含<script setup>
開始標(biāo)簽前面的所有內(nèi)容,也就是刪除掉template
模塊的內(nèi)容和<script setup>
開始標(biāo)簽。這行代碼執(zhí)行完后我們再看看ctx.s.toString()
的值:
接著執(zhí)行ctx.s.remove(endOffset, source.length);
,這行代碼的意思是將</script >
包含結(jié)束標(biāo)簽后面的內(nèi)容全部刪掉,也就是刪除</script >
結(jié)束標(biāo)簽和<style>
模塊。這行代碼執(zhí)行完后我們再來看看ctx.s.toString()
的值:
由于我們的common-child.vue
的script
模塊中只有一個defineProps
函數(shù),所以當(dāng)移除掉template
模塊、style
模塊、script
開始標(biāo)簽和結(jié)束標(biāo)簽后就變成了一個空字符串。如果你的script
模塊中還有其他js
業(yè)務(wù)代碼,當(dāng)代碼執(zhí)行到這里后就不會是空字符串,而是那些js
業(yè)務(wù)代碼。
我們接著將compileScript
函數(shù)中的斷點(diǎn)走到調(diào)用genRuntimeProps
函數(shù)處,代碼如下:
let runtimeOptions = ``; const propsDecl = genRuntimeProps(ctx); if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`;
從genRuntimeProps
名字你應(yīng)該已經(jīng)猜到了他的作用,根據(jù)ctx
上下文生成運(yùn)行時(shí)的props
。我們將斷點(diǎn)走到genRuntimeProps
函數(shù)內(nèi)部,在我們這個場景中genRuntimeProps
主要執(zhí)行的代碼如下:
function genRuntimeProps(ctx) { let propsDecls; if (ctx.propsRuntimeDecl) { propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim(); } return propsDecls; }
還記得這個ctx.propsRuntimeDecl
是什么東西嗎?我們在執(zhí)行processDefineProps
函數(shù)判斷當(dāng)前節(jié)點(diǎn)是否為執(zhí)行defineProps
函數(shù)的時(shí)候,就將調(diào)用defineProps
函數(shù)的參數(shù)node
節(jié)點(diǎn)賦值給ctx.propsRuntimeDecl
。換句話說ctx.propsRuntimeDecl
中擁有調(diào)用defineProps
函數(shù)傳入的props
參數(shù)中的節(jié)點(diǎn)信息。我們將斷點(diǎn)走進(jìn)ctx.getString
函數(shù)看看是如何取出props
的:
getString(node, scriptSetup = true) { const block = scriptSetup ? this.descriptor.scriptSetup : this.descriptor.script; return block.content.slice(node.start, node.end); }
我們前面已經(jīng)講過了descriptor
對象是由vue
文件編譯而來,其中的scriptSetup
屬性就是對應(yīng)的<script setup>
模塊。我們這里沒有傳入scriptSetup
,所以block
的值為this.descriptor.scriptSetup
。同樣我們前面也講過scriptSetup.content
的值是<script setup>
模塊code
代碼字符串。請看下圖:
這里傳入的node
節(jié)點(diǎn)就是我們前面存在上下文中ctx.propsRuntimeDecl
,也就是在調(diào)用defineProps
函數(shù)時(shí)傳入的參數(shù)節(jié)點(diǎn),node.start
就是參數(shù)節(jié)點(diǎn)開始的位置,node.end
就是參數(shù)節(jié)點(diǎn)的結(jié)束位置。所以使用content.slice
方法就可以截取出來調(diào)用defineProps
函數(shù)時(shí)傳入的props
定義。請看下圖:
現(xiàn)在我們再回過頭來看compileScript
函數(shù)中的調(diào)用genRuntimeProps
函數(shù)的代碼你就能很容易理解了:
let runtimeOptions = ``; const propsDecl = genRuntimeProps(ctx); if (propsDecl) runtimeOptions += `\n props: ${propsDecl},`;
這里的propsDecl
在我們這個場景中就是使用slice
截取出來的props
定義,再使用\n props: ${propsDecl},
進(jìn)行字符串拼接就得到了runtimeOptions
的值。如圖:
看到runtimeOptions
的值是不是就覺得很熟悉了,又有name
屬性,又有props
屬性。其實(shí)就是vue
組件對象的code
字符串的一部分。name
拼接邏輯是在省略的代碼中,我們這篇文章只講props
相關(guān)的邏輯,所以name
不在這篇文章的討論范圍內(nèi)。
現(xiàn)在我們能夠回答前面提的第三個問題了。
defineProps
是如何將聲明的 props
自動暴露給模板?
編譯時(shí)在移除掉defineProps
相關(guān)代碼時(shí)會將調(diào)用defineProps
函數(shù)時(shí)傳入的參數(shù)node
節(jié)點(diǎn)信息存到ctx
上下文中。遍歷完AST抽象語法樹后
,然后從上下文中存的參數(shù)node
節(jié)點(diǎn)信息中拿到調(diào)用defineProps
宏函數(shù)時(shí)傳入props
的開始位置和結(jié)束位置。再使用slice
方法并且傳入開始位置和結(jié)束位置,從<script setup>
模塊的代碼字符串中截取到props
定義的字符串。然后將截取到的props
定義的字符串拼接到vue
組件對象的字符串中,這樣vue
組件對象中就有了一個props
屬性,這個props
屬性在template
模版中可以直接使用。
拼接成完整的瀏覽器運(yùn)行時(shí)js
代碼
我們再來看compileScript
函數(shù)中的最后一坨代碼;
const def = (defaultExport ? `\n ...${normalScriptDefaultVar},` : ``) + (definedOptions ? `\n ...${definedOptions},` : ""); ctx.s.prependLeft( startOffset, `\n${genDefaultAs} /*#__PURE__*/${ctx.helper( `defineComponent` )}({${def}${runtimeOptions}\n ${ hasAwait ? `async ` : `` }setup(${args}) {\n${exposeCall}` ); ctx.s.appendRight(endOffset, `})`); return { //.... content: ctx.s.toString(), };
這里先調(diào)用了ctx.s.prependLeft
方法給字符串開始的地方插入了一串字符串,這串拼接的字符串看著腦瓜子痛,我們直接在debug console
上面看看要拼接的字符串是什么樣的:
看到這串你應(yīng)該很熟悉,除了前面我們拼接的name
和props
之外還有部分setup
編譯后的代碼,其實(shí)這就是vue
組件對象的code
代碼字符串的一部分。
當(dāng)斷點(diǎn)執(zhí)行完prependLeft
方法后,我們在debug console
中再看看此時(shí)的ctx.s.toString()
的值是什么樣的:
從圖上可以看到vue
組件對象上的name
屬性、props
屬性、setup
函數(shù)基本已經(jīng)拼接的差不多了,只差一個})
結(jié)束符號,所以執(zhí)行ctx.s.appendRight(endOffset,
}));
將結(jié)束符號插入進(jìn)去。
我們最后再來看看compileScript
函數(shù)的返回對象中的content
屬性,也就是ctx.s.toString()
,content
屬性的值就是vue
組件中的<script setup>
模塊編譯成瀏覽器可執(zhí)行的js
代碼字符串。
為什么不能在非setup
頂層使用defineProps
?
同樣的套路我們來debug
看看if-child.vue
文件,先來回憶一下if-child.vue
文件的代碼。
<template> <div>content is {{ content }}</div> </template> <script setup lang="ts"> import { ref } from "vue"; const count = ref(10); if (count.value) { defineProps({ content: String, }); } </script>
將斷點(diǎn)走到compileScript
函數(shù)的遍歷AST抽象語法樹
的地方,我們看到scriptSetupAst.body
數(shù)組中有三個node
節(jié)點(diǎn)。
從圖中我們可以看到這三個node
節(jié)點(diǎn)類型分別是:ImportDeclaration
、VariableDeclaration
、IfStatement
。很明顯這三個節(jié)點(diǎn)對應(yīng)的是我們源代碼中的import
語句、const
定義變量、if
模塊。我們再來回憶一下compileScript
函數(shù)中的遍歷AST抽象語法樹
的代碼:
function compileScript(sfc, options) { // 省略.. for (const node of scriptSetupAst.body) { if (node.type === "ExpressionStatement") { const expr = node.expression; if (processDefineProps(ctx, expr)) { ctx.s.remove(node.start + startOffset, node.end + startOffset); } } if ( (node.type === "VariableDeclaration" && !node.declare) || node.type.endsWith("Statement") ) { // .... } } // 省略.. }
從代碼我們就可以看出來第三個node
節(jié)點(diǎn),也就是在if
中使用defineProps
的代碼,這個節(jié)點(diǎn)類型為IfStatement
,不等于ExpressionStatement
,所以代碼不會走到processDefineProps
函數(shù)中,也不會執(zhí)行remove
方法刪除掉調(diào)用defineProps
函數(shù)的代碼。當(dāng)代碼運(yùn)行在瀏覽器時(shí)由于我們沒有從任何地方import
導(dǎo)入defineProps
,當(dāng)然就會報(bào)錯defineProps is not defined
。
總結(jié)
現(xiàn)在我們能夠回答前面提的三個問題了。
- 為什么
defineProps
不需要import
導(dǎo)入?
因?yàn)樵诰幾g過程中如果當(dāng)前AST抽象語法樹
的節(jié)點(diǎn)類型是ExpressionStatement
表達(dá)式語句,并且調(diào)用的函數(shù)是defineProps
,那么就調(diào)用remove
方法將調(diào)用defineProps
函數(shù)的代碼給移除掉。既然defineProps
語句已經(jīng)被移除了,自然也就不需要import
導(dǎo)入了defineProps
了。
- 為什么不能在非
setup
頂層使用defineProps
?
因?yàn)樵诜?code>setup頂層使用defineProps
的代碼生成AST抽象語法樹
后節(jié)點(diǎn)類型就不是ExpressionStatement
表達(dá)式語句類型,只有ExpressionStatement
表達(dá)式語句類型才會走到processDefineProps
函數(shù)中,并且調(diào)用remove
方法將調(diào)用defineProps
函數(shù)的代碼給移除掉。當(dāng)代碼運(yùn)行在瀏覽器時(shí)由于我們沒有從任何地方import
導(dǎo)入defineProps
,當(dāng)然就會報(bào)錯defineProps is not defined
。
defineProps
是如何將聲明的props
自動暴露給模板?
編譯時(shí)在移除掉defineProps
相關(guān)代碼時(shí)會將調(diào)用defineProps
函數(shù)時(shí)傳入的參數(shù)node
節(jié)點(diǎn)信息存到ctx
上下文中。遍歷完AST抽象語法樹后
,然后從上下文中存的參數(shù)node
節(jié)點(diǎn)信息中拿到調(diào)用defineProps
宏函數(shù)時(shí)傳入props
的開始位置和結(jié)束位置。再使用slice
方法并且傳入開始位置和結(jié)束位置,從<script setup>
模塊的代碼字符串中截取到props
定義的字符串。然后將截取到的props
定義的字符串拼接到vue
組件對象的字符串中,這樣vue
組件對象中就有了一個props
屬性,這個props
屬性在template
模版中可以直接使用。
到此這篇關(guān)于defineProps宏函數(shù)不需要從vue中import導(dǎo)入的原因解析的文章就介紹到這了,更多相關(guān)vue import導(dǎo)入內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vite使用unplugin-auto-import實(shí)現(xiàn)vue3中的自動導(dǎo)入
- vue3項(xiàng)目如何配置按需自動導(dǎo)入API組件unplugin-auto-import
- vue中import導(dǎo)入三種方式詳解
- vue異步組件與組件懶加載問題(import不能導(dǎo)入變量字符串路徑)
- Vue export import 導(dǎo)入導(dǎo)出的多種方式與區(qū)別介紹
- 解決vue3?defineProps?引入定義的接口報(bào)錯
- 一文詳細(xì)聊聊vue3的defineProps、defineEmits和defineExpose
- Vue3中defineEmits、defineProps?不用引入便直接用
- vue3的defineExpose宏函數(shù)是如何暴露方法給父組件使用
相關(guān)文章
如何使用el-cascader組件寫下拉級聯(lián)多選及全選功能
這篇文章主要介紹了如何使用el-cascader組件寫下拉級聯(lián)多選及全選功能,因?yàn)槭怯腥x的功能,所以不能直接使用el-cascader組件,?而是選擇使用el-select組件,?在此組件內(nèi)部使用el-cascader-panel級聯(lián)面板,感興趣的朋友跟隨小編一起看看吧2024-03-03vue中的addEventListener和removeEventListener用法說明
這篇文章主要介紹了vue中的addEventListener和removeEventListener用法說明,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06element表單驗(yàn)證如何清除校驗(yàn)提示語
本文主要介紹了element表單驗(yàn)證如何清除校驗(yàn)提示語,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10Vue 動態(tài)路由的實(shí)現(xiàn)及 Springsecurity 按鈕級別的權(quán)限控制
這篇文章主要介紹了Vue 動態(tài)路由的實(shí)現(xiàn)以及 Springsecurity 按鈕級別的權(quán)限控制的相關(guān)知識,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09