Vue開發手冊Function-based API RFC
概要
2020 年一月又註:RFC 已經被完全重寫,最新版本請以 https://composition-api.vuejs.org/ 為準。以下內容會有部分與最新的 API 有出入,但依然可以幫助理解。
譯註:這是 3.0 最重要的 RFC,因此特意翻譯成中文。
將 2.x 中與組件邏輯相關的選項以 API 函數的形式重新設計。
基本例子
import { ref, computed, watch, onMounted } from 'vue' const App = { template: ` <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> `, setup() { // reactive state const count = ref(0) // computed state const plusOne = computed(() => count.value + 1) // method const increment = () => { count.value++ } // watch watch(() => count.value * 2, val => { console.log(`count * 2 is ${val}`) }) // lifecycle onMounted(() => { console.log(`mounted`) }) // expose bindings on render context return { count, plusOne, increment } } }
設計動機
邏輯組合與復用
組件 API 設計所面對的核心問題之一就是如何組織邏輯,以及如何在多個組件之間抽取和復用邏輯。基於 Vue 2.x 目前的 API 我們有一些常見的邏輯復用模式,但都或多或少存在一些問題。這些模式包括:
- Mixins
- 高階組件 (Higher-order Components, aka HOCs)
- Renderless Components (基於 scoped slots / 作用域插槽封裝邏輯的組件)
網絡上關於這些模式的介紹很多,這裡就不再贅述細節。總體來說,以上這些模式存在以下問題:
- 模版中的數據來源不清晰。舉例來說,當一個組件中使用瞭多個 mixin 的時候,光看模版會很難分清一個屬性到底是來自哪一個 mixin。HOC 也有類似的問題。
- 命名空間沖突。由不同開發者開發的 mixin 無法保證不會正好用到一樣的屬性或是方法名。HOC 在註入的 props 中也存在類似問題。
- 性能。HOC 和 Renderless Components 都需要額外的組件實例嵌套來封裝邏輯,導致無謂的性能開銷。
Function-based API 受 React Hooks 的啟發,提供瞭一個全新的邏輯復用方案,且不存在上述問題。使用基於函數的 API,我們可以將相關聯的代碼抽取到一個 "composition function"(組合函數)中 —— 該函數封裝瞭相關聯的邏輯,並將需要暴露給組件的狀態以響應式的數據源的方式返回出來。這裡是一個用組合函數來封裝鼠標位置偵聽邏輯的例子:
function useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } // 在組件中使用該函數 const Component = { setup() { const { x, y } = useMouse() // 與其它函數配合使用 const { z } = useOtherLogic() return { x, y, z } }, template: `<div>{{ x }} {{ y }} {{ z }}</div>` }
從以上例子中可以看到:
- 暴露給模版的屬性來源清晰(從函數返回);
- 返回值可以被任意重命名,所以不存在命名空間沖突;
- 沒有創建額外的組件實例所帶來的性能損耗。
文末附錄中有與 React Hooks 的一些細節對比。
類型推導
3.0 的一個主要設計目標是增強對 TypeScript 的支持。原本我們期望通過 Class API 來達成這個目標,但是經過討論和原型開發,我們認為 Class 並不是解決這個問題的正確路線,基於 Class 的 API 依然存在類型問題。
基於函數的 API 天然對類型推導很友好,因為 TS 對函數的參數、返回值和泛型的支持已經非常完備。更值得一提的是基於函數的 API 在使用 TS 或是原生 JS 時寫出來的代碼幾乎是完全一樣的。下文會提供新 API 類型推導的更多細節,此外文末附錄中有關於 Class API 類型問題的更多細節。
打包尺寸
基於函數的 API 每一個函數都可以作為 named ES export 被單獨引入,這使得它們對 tree-shaking 非常友好。沒有被使用的 API 的相關代碼可以在最終打包時被移除。同時,基於函數 API 所寫的代碼也有更好的壓縮效率,因為所有的函數名和 setup 函數體內部的變量名都可以被壓縮,但對象和 class 的屬性/方法名卻不可以。
設計細節
setup() 函數
我們將會引入一個新的組件選項,setup()
。顧名思義,這個函數將會是我們 setup 我們組件邏輯的地方,它會在一個組件實例被創建時,初始化瞭 props 之後調用。setup()
會接收到初始的 props 作為參數:
const MyComponent = { props: { name: String }, setup(props) { console.log(props.name) } }
需要留意的是這裡傳進來的 props
對象是響應式的 —— 它可以被當作數據源去觀測,當後續 props 發生變動時它也會被框架內部同步更新。但對於用戶代碼來說,它是不可修改的(會導致警告)。
在 setup
內部可以使用 this
,但你大部分時候不會需要它。
組件狀態
類似 data()
,setup()
可以返回一個對象 —— 這個對象上的屬性將會被暴露給模版的渲染上下文:
const MyComponent = { props: { name: String }, setup(props) { return { msg: `hello ${props.name}!` } }, template: `<div>{{ msg }}</div>` }
上面這個例子跟 data()
一模一樣:msg
可以在模版中被直接使用,它甚至可以被模版中的內聯函數修改。但如果我們想要創建一個可以在 setup()
內部被管理的值,可以使用 ref
函數:
import { ref } from 'vue' const MyComponent = { setup(props) { const msg = ref('hello') const appendName = () => { msg.value = `hello ${props.name}` } return { msg, appendName } }, template: `<div @click="appendName">{{ msg }}</div>` }
ref()
返回的是一個 value reference (包裝對象)。一個包裝對象隻有一個屬性:.value
,該屬性指向內部被包裝的值。在上面的例子中,msg
包裝的是一個字符串。包裝對象的值可以被直接修改:
// 讀取 console.log(msg.value) // 'hello' // 修改 msg.value = 'bye'
為什麼需要包裝對象?
我們知道在 JavaScript 中,原始值類型如 string 和 number 是隻有值,沒有引用的。如果在一個函數中返回一個字符串變量,接收到這個字符串的代碼隻會獲得一個值,是無法追蹤原始變量後續的變化的。
因此,包裝對象的意義就在於提供一個讓我們能夠在函數之間以引用的方式傳遞任意類型值的容器。這有點像 React Hooks 中的 useRef
—— 但不同的是 Vue 的包裝對象同時還是響應式的數據源。有瞭這樣的容器,我們就可以在封裝瞭邏輯的組合函數中將狀態以引用的方式傳回給組件。組件負責展示(追蹤依賴),組合函數負責管理狀態(觸發更新):
setup() { const valueA = useLogicA() // valueA 可能被 useLogicA() 內部的代碼修改從而觸發更新 const valueB = useLogicB() return { valueA, valueB } }
包裝對象也可以包裝非原始值類型的數據,被包裝的對象中嵌套的屬性都會被響應式地追蹤。用包裝對象去包裝對象或是數組並不是沒有意義的:它讓我們可以對整個對象的值進行替換 —— 比如用一個 filter 過的數組去替代原數組:
const numbers = ref([1, 2, 3]) // 替代原數組,但引用不變 numbers.value = numbers.value.filter(n => n > 1)
如果你依然想創建一個沒有包裝的響應式對象,可以使用 reactive
API(和 2.x 的 Vue.observable()
等同):
import { reactive } from 'vue' const object = reactive({ count: 0 }) object.count++
Ref Unwrapping(包裝對象的自動展開)
在上面的一個例子中你可能註意到瞭,雖然 setup()
返回的 msg
是一個包裝對象,但在模版中我們直接用瞭 {{ msg }}
這樣的綁定,沒有用 .value
。這是因為當包裝對象被暴露給模版渲染上下文,或是被嵌套在另一個響應式對象中的時候,它會被自動展開 (unwrap) 為內部的值。
比如一個包裝對象的綁定可以直接被模版中的內聯函數修改:
const MyComponent = { setup() { return { count: ref(0) } }, template: `<button @click="count++">{{ count }}</button>` }
當一個包裝對象被作為另一個響應式對象的屬性引用的時候也會被自動展開:
const count = ref(0) const obj = reactive({ count }) console.log(obj.count) // 0 obj.count++ console.log(obj.count) // 1 console.log(count.value) // 1 count.value++ console.log(obj.count) // 2 console.log(count.value) // 2
以上這些關於包裝對象的細節可能會讓你覺得有些復雜,但實際使用中你隻需要記住一個基本的規則:隻有當你直接以變量的形式引用一個包裝對象的時候才會需要用 .value
去取它內部的值 —— 在模版中你甚至不需要知道它們的存在。
配合手寫 Render 函數使用
如果你的組件不使用模版,你也可以選擇在 setup()
中直接返回一個渲染函數:
import { ref, createElement as h } from 'vue' const MyComponent = { setup(initialProps) { const count = ref(0) const increment = () => { count.value++ } return (props, slots, attrs, vnode) => ( h('button', { onClick: increment }, count.value) ) } }
返回的函數應當遵循 RFC#28 中提出的函數簽名。你可能註意到瞭 setup()
和其返回的渲染函數的第一個參數都是 props —— 它們的行為是一樣的,但是渲染函數接收到的 props 在生產模式下將會是一個普通對象,因此它的性能會更好些。
和 2.x 一樣的 render
選項也可以使用,但如果用瞭 setup()
,就應該盡量使用內聯返回的渲染函數,因為這樣可以避免先返回一堆綁定然後再在另一個函數裡解構出來,同時類型推導也會更簡單直接一些。
Computed Value (計算值)
除瞭直接包裝一個可變的值,我們也可以包裝通過計算產生的值:
import { ref, computed } from 'vue' const count = ref(0) const countPlusOne = computed(() => count.value + 1) console.log(countPlusOne.value) // 1 count.value++ console.log(countPlusOne.value) // 2
計算值的行為跟計算屬性 (computed property) 一樣:隻有當依賴變化的時候它才會被重新計算。
computed()
返回的是一個隻讀的包裝對象,它可以和普通的包裝對象一樣在 setup()
中被返回 ,也一樣會在渲染上下文中被自動展開。默認情況下,如果用戶試圖去修改一個隻讀包裝對象,會觸發警告。
雙向計算值可以通過傳給 computed
第二個參數作為 setter 來創建:
const count = value(0) const writableComputed = computed( // read () => count.value + 1, // write val => { count.value = val - 1 } )
Watchers
watch()
API 提供瞭基於觀察狀態的變化來執行副作用的能力。
watch()
接收的第一個參數被稱作 “數據源”,它可以是:
- 一個返回任意值的函數
- 一個包裝對象
- 一個包含上述兩種數據源的數組
第二個參數是回調函數。回調函數隻有當數據源發生變動時才會被觸發:
watch( // getter () => count.value + 1, // callback (value, oldValue) => { console.log('count + 1 is: ', value) } ) // -> count + 1 is: 1 count.value++ // -> count + 1 is: 2
和 2.x 的 $watch
有所不同的是,watch()
的回調會在創建時就執行一次。這有點類似 2.x watcher 的 immediate: true
選項,但有一個重要的不同:默認情況下 watch()
的回調總是會在當前的 renderer flush 之後才被調用 —— 換句話說,watch()
的回調在觸發時,DOM 總是會在一個已經被更新過的狀態下。 這個行為是可以通過選項來定制的。
在 2.x 的代碼中,我們經常會遇到同一份邏輯需要在 mounted
和一個 watcher 的回調中執行(比如根據當前的 id 抓取數據),3.0 的 watch()
默認行為可以直接表達這樣的需求。
觀察 props
上面提到瞭 setup()
接收到的 props
對象是一個可觀測的響應式對象:
const MyComponent = { props: { id: Number }, setup(props) { const data = ref(null) watch(() => props.id, async (id) => { data.value = await fetchData(id) }) return { data } } }
觀察包裝對象
watch()
可以直接觀察一個包裝對象:
// double 是一個計算包裝對象 const double = computed(() => count.value * 2) watch(double, value => { console.log('double the count is: ', value) }) // -> double the count is: 0 count.value++ // -> double the count is: 2
觀察多個數據源
watch()
也可以觀察一個包含多個數據源的數組 – 這種情況下,任意一個數據源的變化都會觸發回調,同時回調會接收到包含對應值的數組作為參數:
watch( [refA, () => refB.value], ([a, b], [prevA, prevB]) => { console.log(`a is: ${a}`) console.log(`b is: ${b}`) } )
停止觀察
watch()
返回一個停止觀察的函數:
const stop = watch(...) // stop watching stop()
如果 watch()
是在一個組件的 setup()
或是生命周期函數中被調用的,那麼該 watcher 會在當前組件被銷毀時也一同被自動停止:
export default { setup() { // 組件銷毀時也會被自動停止 watch(/* ... */) } }
清理副作用
有時候當觀察的數據源變化後,我們可能需要對之前所執行的副作用進行清理。舉例來說,一個異步操作在完成之前數據就產生瞭變化,我們可能要撤銷還在等待的前一個操作。為瞭處理這種情況,watcher 的回調會接收到的第三個參數是一個用來註冊清理操作的函數。調用這個函數可以註冊一個清理函數。清理函數會在下屬情況下被調用:
- 在回調被下一次調用前
- 在 watcher 被停止前
watch(idValue, (id, oldId, onCleanup) => { const token = performAsyncOperation(id) onCleanup(() => { // id 發生瞭變化,或是 watcher 即將被停止. // 取消還未完成的異步操作。 token.cancel() }) })
之所以要用傳入的註冊函數來註冊清理函數,而不是像 React 的 useEffect
那樣直接返回一個清理函數,是因為 watcher 回調的返回值在異步場景下有特殊作用。我們經常需要在 watcher 的回調中用 async function 來執行異步操作:
const data = ref(null) watch(getId, async (id) => { data.value = await fetchData(id) })
我們知道 async function 隱性地返回一個 Promise – 這樣的情況下,我們是無法返回一個需要被立刻註冊的清理函數的。除此之外,回調返回的 Promise 還會被 Vue 用於內部的異步錯誤處理。
Watcher 回調的調用時機
默認情況下,所有的 watcher 回調都會在當前的 renderer flush 之後被調用。這確保瞭在回調中 DOM 永遠都已經被更新完畢。如果你想要讓回調在 DOM 更新之前或是被同步觸發,可以使用 flush
選項:
watch( () => count.value + 1, () => console.log(`count changed`), { flush: 'post', // default, fire after renderer flush flush: 'pre', // fire right before renderer flush flush: 'sync' // fire synchronously } )
全部的 watch 選項(TS 類型聲明)
interface WatchOptions { lazy?: boolean deep?: boolean flush?: 'pre' | 'post' | 'sync' onTrack?: (e: DebuggerEvent) => void onTrigger?: (e: DebuggerEvent) => void } interface DebuggerEvent { effect: ReactiveEffect target: any key: string | symbol | undefined type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate' }
lazy
與 2.x 的immediate
正好相反deep
與 2.x 行為一致onTrack
和onTrigger
是兩個用於 debug 的鉤子,分別在 watcher 追蹤到依賴和依賴發生變化的時候被調用,獲得的參數是一個包含瞭依賴細節的 debugger event。
生命周期函數
所有現有的生命周期鉤子都會有對應的 onXXX
函數(隻能在 setup()
中使用):
import { onMounted, onUpdated, onUnmounted } from 'vue' const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) // destroyed 調整為 unmounted onUnmounted(() => { console.log('unmounted!') }) } }
依賴註入
import { provide, inject } from 'vue' const CountSymbol = Symbol() const Ancestor = { setup() { // providing a ref can make it reactive const count = ref(0) provide(CountSymbol, count) } } const Descendent = { setup() { const count = inject(CountSymbol) return { count } } }
如果註入的是一個包裝對象,則該註入綁定會是響應式的(也就是說,如果 Ancestor 修改瞭 count,會觸發 Descendent 的更新)。
類型推導
為瞭能夠在 TypeScript 中提供正確的類型推導,我們需要通過一個函數來定義組件:
import { defineComponent, ref } from 'vue' const MyComponent = defineComponent({ // props declarations are used to infer prop types props: { msg: String }, setup(props) { props.msg // string | undefined // bindings returned from setup() can be used for type inference // in templates const count = ref(0) return { count } } })
defineComponent
從概念上來說和 2.x 的 Vue.extend
是一樣的,但在 3.0 中它其實是單純為瞭類型推導而存在的,內部實現是個 noop(直接返回參數本身)。它的返回類型可以用於 TSX 和 Vetur 的模版自動補全。如果你使用單文件組件,則 Vetur 可以自動隱式地幫你添加這個調用。
如果你使用手寫 render 函數或是 TSX,那麼你可以在 setup()
當中直接返回一個渲染函數(註意這裡不需要任何手動的類型聲明):
import { defineComponent, ref, h } from 'vue' const MyComponent = defineComponent({ props: { msg: String }, setup(props) { const count = ref(0) return () => h('div', [ h('p', `msg is ${props.msg}`), h('p', `count is ${count.value}`) ]) } })
純 TypeScript 的 Props 類型聲明
3.0 的 props
選項不是必須的,如果你不需要運行時的 props 類型檢查,你也可以選擇完全在 TypeScript 的類型層面聲明 props 的類型:
import { defineComponent, h } from 'vue' interface Props { msg: string } const MyComponent = defineComponent({ setup(props: Props) { return () => h('div', props.msg) } })
如果不需要除瞭 setup
之外的選項,甚至可以直接傳一個函數給 defineComponent
:
const MyComponent = createComponent((props: { msg: string }) => { return () => h('div', props.msg) })
這裡返回的 MyComponent
也可以在 TSX 中提供正確的 props 補全和推導。
Required Props
Props 默認都是可選的,也就是說它們的類型都可能是 undefined
。非可選的 props 需要聲明 required: true
:
import { defineComponent } from 'vue' defineComponent({ props: { foo: { type: String, required: true }, bar: { type: String } }, setup(props) { props.foo // string props.bar // string | undefined } })
復雜 Props 類型
Vue 提供的 PropType
類型可以用來聲明任意復雜度的 props 類型,但需要用 as any
進行一次強制類型轉換:
import { defineComponent, PropType } from 'vue' defineComponent({ props: { options: (null as any) as PropType<{ msg: string }> }, setup(props) { props.options // { msg: string } | undefined } })
依賴註入類型
依賴註入的 inject
方法是唯一必須手動聲明類型的 API:
import { defineComponent, inject, Ref } from 'vue' defineComponent({ setup() { const count: Ref<number> = inject(CountSymbol) return { count } } })
這裡的 Ref
類型即是包裝對象的類型 ,通過泛型參數來聲明其內部包裝的值的類型。
缺點/潛在問題
新的 API 使得動態地檢視/修改一個組件的選項變得更困難(原來是一個對象,現在是一段無法被檢視的函數體)。
這可能是一件好事,因為通常在用戶代碼中動態地檢視/修改組件是一類比較危險的操作,對於運行時也增加瞭許多潛在的邊緣情況(特別是組件繼承和使用 mixin 的情況下)。新 API 的靈活性應該在絕大部分情況下都可以用更顯式的代碼達成同樣的結果。
缺乏經驗的用戶可能會寫出 “面條代碼”,因為新 API 不像舊 API 那樣強制將組件代碼基於選項切分開來。
我們在 Class API RFC 和內部討論中聽到過好幾次這樣的聲音,但我認為這是一種沒有必要的擔憂。雖然理論上新的 API 確實制約更少,但我認為 “面條代碼” 的情況不太可能發生,這裡詳細解釋一下。
基於函數的新 API 和基於選項的舊 API 之間的最大區別,就是新 API 讓抽取邏輯變得非常簡單 —— 就跟在普通的代碼中抽取函數一樣。也就是說,我們不必隻在需要復用邏輯的時候才抽取函數,也可以單純為瞭更好地組織代碼去抽取函數。
基於選項的代碼隻是看上去更整潔。一個復雜的組件往往需要同時處理多個不同的邏輯任務,每個邏輯任務所涉及的代碼在選項 API 下是被分散在多個選項之中的。舉例來說,從服務端抓取一份數據,可能需要用到 props
, data()
, mounted
和 watch
。極端情況下,如果我們把一個應用中所有的邏輯任務都放在一個組件裡,這個組件必然會變得龐大而難以維護,因為每個邏輯任務的代碼都被選項切成瞭多個碎片分散在各處。
對比之下,基於函數的 API 讓我們可以把每個邏輯任務的代碼都整理到一個對應的函數中。當我們發現一個組件變得過大時,我們會將它切分成多個更小的組件;同樣地,如果一個組件的 setup()
函數變得很復雜,我們可以將它切分成多個更小的函數。而如果是基於選項,則無法做到這樣的切分,因為用 mixin 隻會讓事情變得更糟糕。
從這個角度看,基於選項 vs. 基於函數就好像基於 HTML/CSS/JS 組織代碼 vs. 基於單文件組件來組織代碼。
升級策略
新的 API 和 2.x 的 API 完全兼容(隻是多瞭一個 setup()
選項) ,並且可以一起使用。新 API 適合在組件復雜度明顯的情況下用來更好的組織和復用邏輯,或是用來提供可高度復用的插件。
理論上,新 API 可以完全提供 2.x API 的全部能力,因此我們可能會提供一個可選的編譯時選項,啟用後可以去掉所有僅為 2.x API 支持而存在的代碼,減少一部分體積和性能開銷。
附錄
與 React Hooks 的對比
這裡提出的 API 和 React Hooks 有一定的相似性,具有同等的基於函數抽取和復用邏輯的能力,但也有很本質的區別。React Hooks 在每次組件渲染時都會調用,通過隱式地將狀態掛載在當前的內部組件節點上,在下一次渲染時根據調用順序取出。而 Vue 的 setup()
每個組件實例隻會在初始化時調用一次 ,狀態通過引用儲存在 setup()
的閉包內。這意味著基於 Vue 的函數 API 的代碼:
- 整體上更符合 JavaScript 的直覺;
- 不受調用順序的限制,可以有條件地被調用;
- 不會在後續更新時不斷產生大量的內聯函數而影響引擎優化或是導致 GC 壓力;
- 不需要總是使用
useCallback
來緩存傳給子組件的回調以防止過度更新; - 不需要擔心傳瞭錯誤的依賴數組給
useEffect/useMemo/useCallback
從而導致回調中使用瞭過期的值 —— Vue 的依賴追蹤是全自動的。
註:React Hooks 的開創性毋庸置疑,也是本提案的靈感來源。Hooks 代碼和 JSX 並置使得對值的使用更簡潔也是其優點,但其設計確實存在上述問題,而 Vue 的響應式系統恰巧能夠讓我們繞過這些問題。
Class API 的類型問題
Class API 提案的主要目的是尋找一個能夠提供更好的 TypeScript 支持的組件聲明方式。但是由於 Vue 需要將來自多個選項的屬性混合到同一個渲染上下文上,這使得即使用瞭 Class,要得到良好的類型推導也不是很容易。
以 props 的類型推導為例。要將 props 的類型 merge 到 class 的 this
上,我們有兩個選擇:用 class 的泛型參數,或是用 decorator。
這是用泛型參數的例子:
interface Props { message: string } class App extends Component<Props> { static props = { message: String } }
由於泛型參數是純類型層面的,所以我們還需要額外地進行一次運行時的 props 選項聲明來獲得正確的行為。這就導致需要進行雙重聲明。
使用 decorator 的例子如下:
class App extends Component<Props> { @prop message: string }
Decorators 存在如下問題:
- ES 的 decorator 提案仍然在 stage-2 且極其不穩定。過去一年內已經經歷瞭兩次徹底大改,且和 TS 現有的實現已經完全脫節。現在引入一個基於 TS decorator 實現的 API 風險太大。
- Decorator 隻能聲明 class
this
上的屬性,卻無法將某一類 decorator 聲明的屬性歸並到一個對象上(比如$props
),這就導致this.$props
無法被推導,且影響 TSX 的使用。 - 用戶很可能會覺得可以用
@prop message: string = 'foo'
這樣的寫法去聲明默認值,但事實上技術層面無法做到符合語義的實現。
最後,class 還有一個問題,那就是目前 class method 不支持參數的 contextual typing,也就是說我們無法基於 class 本身的 fields 來推導某個 method 的參數類型,需要用戶自己去聲明。
以上就是Vue開發手冊Function-based API RFC的詳細內容,更多關於Vue Function-based API RFC的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Vue3 Composition API的使用簡介
- vue3 文檔梳理快速入門
- Hooks封裝與使用示例詳解
- 一文掌握在Vue3中書寫TSX的使用方法
- Vue3編程流暢技巧使用setup語法糖拒絕寫return