Vue開發(fā)手冊Function-based?API?RFC
概要
2020 年一月又注:RFC 已經(jīng)被完全重寫,最新版本請以 https://composition-api.vuejs.org/ 為準。以下內(nèi)容會有部分與最新的 API 有出入,但依然可以幫助理解。
譯注:這是 3.0 最重要的 RFC,因此特意翻譯成中文。
將 2.x 中與組件邏輯相關(guān)的選項以 API 函數(shù)的形式重新設(shè)計。
基本例子
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 } } }
設(shè)計動機
邏輯組合與復(fù)用
組件 API 設(shè)計所面對的核心問題之一就是如何組織邏輯,以及如何在多個組件之間抽取和復(fù)用邏輯?;?Vue 2.x 目前的 API 我們有一些常見的邏輯復(fù)用模式,但都或多或少存在一些問題。這些模式包括:
- Mixins
- 高階組件 (Higher-order Components, aka HOCs)
- Renderless Components (基于 scoped slots / 作用域插槽封裝邏輯的組件)
網(wǎng)絡(luò)上關(guān)于這些模式的介紹很多,這里就不再贅述細節(jié)??傮w來說,以上這些模式存在以下問題:
- 模版中的數(shù)據(jù)來源不清晰。舉例來說,當一個組件中使用了多個 mixin 的時候,光看模版會很難分清一個屬性到底是來自哪一個 mixin。HOC 也有類似的問題。
- 命名空間沖突。由不同開發(fā)者開發(fā)的 mixin 無法保證不會正好用到一樣的屬性或是方法名。HOC 在注入的 props 中也存在類似問題。
- 性能。HOC 和 Renderless Components 都需要額外的組件實例嵌套來封裝邏輯,導(dǎo)致無謂的性能開銷。
Function-based API 受 React Hooks 的啟發(fā),提供了一個全新的邏輯復(fù)用方案,且不存在上述問題。使用基于函數(shù)的 API,我們可以將相關(guān)聯(lián)的代碼抽取到一個 "composition function"(組合函數(shù))中 —— 該函數(shù)封裝了相關(guān)聯(lián)的邏輯,并將需要暴露給組件的狀態(tài)以響應(yīng)式的數(shù)據(jù)源的方式返回出來。這里是一個用組合函數(shù)來封裝鼠標位置偵聽邏輯的例子:
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 } } // 在組件中使用該函數(shù) const Component = { setup() { const { x, y } = useMouse() // 與其它函數(shù)配合使用 const { z } = useOtherLogic() return { x, y, z } }, template: `<div>{{ x }} {{ y }} {{ z }}</div>` }
從以上例子中可以看到:
- 暴露給模版的屬性來源清晰(從函數(shù)返回);
- 返回值可以被任意重命名,所以不存在命名空間沖突;
- 沒有創(chuàng)建額外的組件實例所帶來的性能損耗。
文末附錄中有與 React Hooks 的一些細節(jié)對比。
類型推導(dǎo)
3.0 的一個主要設(shè)計目標是增強對 TypeScript 的支持。原本我們期望通過 Class API 來達成這個目標,但是經(jīng)過討論和原型開發(fā),我們認為 Class 并不是解決這個問題的正確路線,基于 Class 的 API 依然存在類型問題。
基于函數(shù)的 API 天然對類型推導(dǎo)很友好,因為 TS 對函數(shù)的參數(shù)、返回值和泛型的支持已經(jīng)非常完備。更值得一提的是基于函數(shù)的 API 在使用 TS 或是原生 JS 時寫出來的代碼幾乎是完全一樣的。下文會提供新 API 類型推導(dǎo)的更多細節(jié),此外文末附錄中有關(guān)于 Class API 類型問題的更多細節(jié)。
打包尺寸
基于函數(shù)的 API 每一個函數(shù)都可以作為 named ES export 被單獨引入,這使得它們對 tree-shaking 非常友好。沒有被使用的 API 的相關(guān)代碼可以在最終打包時被移除。同時,基于函數(shù) API 所寫的代碼也有更好的壓縮效率,因為所有的函數(shù)名和 setup 函數(shù)體內(nèi)部的變量名都可以被壓縮,但對象和 class 的屬性/方法名卻不可以。
設(shè)計細節(jié)
setup() 函數(shù)
我們將會引入一個新的組件選項,setup()
。顧名思義,這個函數(shù)將會是我們 setup 我們組件邏輯的地方,它會在一個組件實例被創(chuàng)建時,初始化了 props 之后調(diào)用。setup()
會接收到初始的 props 作為參數(shù):
const MyComponent = { props: { name: String }, setup(props) { console.log(props.name) } }
需要留意的是這里傳進來的 props
對象是響應(yīng)式的 —— 它可以被當作數(shù)據(jù)源去觀測,當后續(xù) props 發(fā)生變動時它也會被框架內(nèi)部同步更新。但對于用戶代碼來說,它是不可修改的(會導(dǎo)致警告)。
在 setup
內(nèi)部可以使用 this
,但你大部分時候不會需要它。
組件狀態(tài)
類似 data()
,setup()
可以返回一個對象 —— 這個對象上的屬性將會被暴露給模版的渲染上下文:
const MyComponent = { props: { name: String }, setup(props) { return { msg: `hello ${props.name}!` } }, template: `<div>{{ msg }}</div>` }
上面這個例子跟 data()
一模一樣:msg
可以在模版中被直接使用,它甚至可以被模版中的內(nèi)聯(lián)函數(shù)修改。但如果我們想要創(chuàng)建一個可以在 setup()
內(nèi)部被管理的值,可以使用 ref
函數(shù):
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
,該屬性指向內(nèi)部被包裝的值。在上面的例子中,msg
包裝的是一個字符串。包裝對象的值可以被直接修改:
// 讀取 console.log(msg.value) // 'hello' // 修改 msg.value = 'bye'
為什么需要包裝對象?
我們知道在 JavaScript 中,原始值類型如 string 和 number 是只有值,沒有引用的。如果在一個函數(shù)中返回一個字符串變量,接收到這個字符串的代碼只會獲得一個值,是無法追蹤原始變量后續(xù)的變化的。
因此,包裝對象的意義就在于提供一個讓我們能夠在函數(shù)之間以引用的方式傳遞任意類型值的容器。這有點像 React Hooks 中的 useRef
—— 但不同的是 Vue 的包裝對象同時還是響應(yīng)式的數(shù)據(jù)源。有了這樣的容器,我們就可以在封裝了邏輯的組合函數(shù)中將狀態(tài)以引用的方式傳回給組件。組件負責(zé)展示(追蹤依賴),組合函數(shù)負責(zé)管理狀態(tài)(觸發(fā)更新):
setup() { const valueA = useLogicA() // valueA 可能被 useLogicA() 內(nèi)部的代碼修改從而觸發(fā)更新 const valueB = useLogicB() return { valueA, valueB } }
包裝對象也可以包裝非原始值類型的數(shù)據(jù),被包裝的對象中嵌套的屬性都會被響應(yīng)式地追蹤。用包裝對象去包裝對象或是數(shù)組并不是沒有意義的:它讓我們可以對整個對象的值進行替換 —— 比如用一個 filter 過的數(shù)組去替代原數(shù)組:
const numbers = ref([1, 2, 3]) // 替代原數(shù)組,但引用不變 numbers.value = numbers.value.filter(n => n > 1)
如果你依然想創(chuàng)建一個沒有包裝的響應(yīng)式對象,可以使用 reactive
API(和 2.x 的 Vue.observable()
等同):
import { reactive } from 'vue' const object = reactive({ count: 0 }) object.count++
Ref Unwrapping(包裝對象的自動展開)
在上面的一個例子中你可能注意到了,雖然 setup()
返回的 msg
是一個包裝對象,但在模版中我們直接用了 {{ msg }}
這樣的綁定,沒有用 .value
。這是因為當包裝對象被暴露給模版渲染上下文,或是被嵌套在另一個響應(yīng)式對象中的時候,它會被自動展開 (unwrap) 為內(nèi)部的值。
比如一個包裝對象的綁定可以直接被模版中的內(nèi)聯(lián)函數(shù)修改:
const MyComponent = { setup() { return { count: ref(0) } }, template: `<button @click="count++">{{ count }}</button>` }
當一個包裝對象被作為另一個響應(yīng)式對象的屬性引用的時候也會被自動展開:
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
以上這些關(guān)于包裝對象的細節(jié)可能會讓你覺得有些復(fù)雜,但實際使用中你只需要記住一個基本的規(guī)則:只有當你直接以變量的形式引用一個包裝對象的時候才會需要用 .value
去取它內(nèi)部的值 —— 在模版中你甚至不需要知道它們的存在。
配合手寫 Render 函數(shù)使用
如果你的組件不使用模版,你也可以選擇在 setup()
中直接返回一個渲染函數(shù):
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) ) } }
返回的函數(shù)應(yīng)當遵循 RFC#28 中提出的函數(shù)簽名。你可能注意到了 setup()
和其返回的渲染函數(shù)的第一個參數(shù)都是 props —— 它們的行為是一樣的,但是渲染函數(shù)接收到的 props 在生產(chǎn)模式下將會是一個普通對象,因此它的性能會更好些。
和 2.x 一樣的 render
選項也可以使用,但如果用了 setup()
,就應(yīng)該盡量使用內(nèi)聯(lián)返回的渲染函數(shù),因為這樣可以避免先返回一堆綁定然后再在另一個函數(shù)里解構(gòu)出來,同時類型推導(dǎo)也會更簡單直接一些。
Computed Value (計算值)
除了直接包裝一個可變的值,我們也可以包裝通過計算產(chǎn)生的值:
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()
中被返回 ,也一樣會在渲染上下文中被自動展開。默認情況下,如果用戶試圖去修改一個只讀包裝對象,會觸發(fā)警告。
雙向計算值可以通過傳給 computed
第二個參數(shù)作為 setter 來創(chuàng)建:
const count = value(0) const writableComputed = computed( // read () => count.value + 1, // write val => { count.value = val - 1 } )
Watchers
watch()
API 提供了基于觀察狀態(tài)的變化來執(zhí)行副作用的能力。
watch()
接收的第一個參數(shù)被稱作 “數(shù)據(jù)源”,它可以是:
- 一個返回任意值的函數(shù)
- 一個包裝對象
- 一個包含上述兩種數(shù)據(jù)源的數(shù)組
第二個參數(shù)是回調(diào)函數(shù)?;卣{(diào)函數(shù)只有當數(shù)據(jù)源發(fā)生變動時才會被觸發(fā):
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()
的回調(diào)會在創(chuàng)建時就執(zhí)行一次。這有點類似 2.x watcher 的 immediate: true
選項,但有一個重要的不同:默認情況下 watch()
的回調(diào)總是會在當前的 renderer flush 之后才被調(diào)用 —— 換句話說,watch()
的回調(diào)在觸發(fā)時,DOM 總是會在一個已經(jīng)被更新過的狀態(tài)下。 這個行為是可以通過選項來定制的。
在 2.x 的代碼中,我們經(jīng)常會遇到同一份邏輯需要在 mounted
和一個 watcher 的回調(diào)中執(zhí)行(比如根據(jù)當前的 id 抓取數(shù)據(jù)),3.0 的 watch()
默認行為可以直接表達這樣的需求。
觀察 props
上面提到了 setup()
接收到的 props
對象是一個可觀測的響應(yīng)式對象:
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
觀察多個數(shù)據(jù)源
watch()
也可以觀察一個包含多個數(shù)據(jù)源的數(shù)組 - 這種情況下,任意一個數(shù)據(jù)源的變化都會觸發(fā)回調(diào),同時回調(diào)會接收到包含對應(yīng)值的數(shù)組作為參數(shù):
watch( [refA, () => refB.value], ([a, b], [prevA, prevB]) => { console.log(`a is: ${a}`) console.log(`b is: $`) } )
停止觀察
watch()
返回一個停止觀察的函數(shù):
const stop = watch(...) // stop watching stop()
如果 watch()
是在一個組件的 setup()
或是生命周期函數(shù)中被調(diào)用的,那么該 watcher 會在當前組件被銷毀時也一同被自動停止:
export default { setup() { // 組件銷毀時也會被自動停止 watch(/* ... */) } }
清理副作用
有時候當觀察的數(shù)據(jù)源變化后,我們可能需要對之前所執(zhí)行的副作用進行清理。舉例來說,一個異步操作在完成之前數(shù)據(jù)就產(chǎn)生了變化,我們可能要撤銷還在等待的前一個操作。為了處理這種情況,watcher 的回調(diào)會接收到的第三個參數(shù)是一個用來注冊清理操作的函數(shù)。調(diào)用這個函數(shù)可以注冊一個清理函數(shù)。清理函數(shù)會在下屬情況下被調(diào)用:
- 在回調(diào)被下一次調(diào)用前
- 在 watcher 被停止前
watch(idValue, (id, oldId, onCleanup) => { const token = performAsyncOperation(id) onCleanup(() => { // id 發(fā)生了變化,或是 watcher 即將被停止. // 取消還未完成的異步操作。 token.cancel() }) })
之所以要用傳入的注冊函數(shù)來注冊清理函數(shù),而不是像 React 的 useEffect
那樣直接返回一個清理函數(shù),是因為 watcher 回調(diào)的返回值在異步場景下有特殊作用。我們經(jīng)常需要在 watcher 的回調(diào)中用 async function 來執(zhí)行異步操作:
const data = ref(null) watch(getId, async (id) => { data.value = await fetchData(id) })
我們知道 async function 隱性地返回一個 Promise - 這樣的情況下,我們是無法返回一個需要被立刻注冊的清理函數(shù)的。除此之外,回調(diào)返回的 Promise 還會被 Vue 用于內(nèi)部的異步錯誤處理。
Watcher 回調(diào)的調(diào)用時機
默認情況下,所有的 watcher 回調(diào)都會在當前的 renderer flush 之后被調(diào)用。這確保了在回調(diào)中 DOM 永遠都已經(jīng)被更新完畢。如果你想要讓回調(diào)在 DOM 更新之前或是被同步觸發(fā),可以使用 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 追蹤到依賴和依賴發(fā)生變化的時候被調(diào)用,獲得的參數(shù)是一個包含了依賴細節(jié)的 debugger event。
生命周期函數(shù)
所有現(xiàn)有的生命周期鉤子都會有對應(yīng)的 onXXX
函數(shù)(只能在 setup()
中使用):
import { onMounted, onUpdated, onUnmounted } from 'vue' const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) // destroyed 調(diào)整為 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 } } }
如果注入的是一個包裝對象,則該注入綁定會是響應(yīng)式的(也就是說,如果 Ancestor 修改了 count,會觸發(fā) Descendent 的更新)。
類型推導(dǎo)
為了能夠在 TypeScript 中提供正確的類型推導(dǎo),我們需要通過一個函數(shù)來定義組件:
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 中它其實是單純?yōu)榱祟愋屯茖?dǎo)而存在的,內(nèi)部實現(xiàn)是個 noop(直接返回參數(shù)本身)。它的返回類型可以用于 TSX 和 Vetur 的模版自動補全。如果你使用單文件組件,則 Vetur 可以自動隱式地幫你添加這個調(diào)用。
如果你使用手寫 render 函數(shù)或是 TSX,那么你可以在 setup()
當中直接返回一個渲染函數(shù)(注意這里不需要任何手動的類型聲明):
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
之外的選項,甚至可以直接傳一個函數(shù)給 defineComponent
:
const MyComponent = createComponent((props: { msg: string }) => { return () => h('div', props.msg) })
這里返回的 MyComponent
也可以在 TSX 中提供正確的 props 補全和推導(dǎo)。
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 } })
復(fù)雜 Props 類型
Vue 提供的 PropType
類型可以用來聲明任意復(fù)雜度的 props 類型,但需要用 as any
進行一次強制類型轉(zhuǎn)換:
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
類型即是包裝對象的類型 ,通過泛型參數(shù)來聲明其內(nèi)部包裝的值的類型。
缺點/潛在問題
新的 API 使得動態(tài)地檢視/修改一個組件的選項變得更困難(原來是一個對象,現(xiàn)在是一段無法被檢視的函數(shù)體)。
這可能是一件好事,因為通常在用戶代碼中動態(tài)地檢視/修改組件是一類比較危險的操作,對于運行時也增加了許多潛在的邊緣情況(特別是組件繼承和使用 mixin 的情況下)。新 API 的靈活性應(yīng)該在絕大部分情況下都可以用更顯式的代碼達成同樣的結(jié)果。
缺乏經(jīng)驗的用戶可能會寫出 “面條代碼”,因為新 API 不像舊 API 那樣強制將組件代碼基于選項切分開來。
我們在 Class API RFC 和內(nèi)部討論中聽到過好幾次這樣的聲音,但我認為這是一種沒有必要的擔(dān)憂。雖然理論上新的 API 確實制約更少,但我認為 “面條代碼” 的情況不太可能發(fā)生,這里詳細解釋一下。
基于函數(shù)的新 API 和基于選項的舊 API 之間的最大區(qū)別,就是新 API 讓抽取邏輯變得非常簡單 —— 就跟在普通的代碼中抽取函數(shù)一樣。也就是說,我們不必只在需要復(fù)用邏輯的時候才抽取函數(shù),也可以單純?yōu)榱烁玫亟M織代碼去抽取函數(shù)。
基于選項的代碼只是看上去更整潔。一個復(fù)雜的組件往往需要同時處理多個不同的邏輯任務(wù),每個邏輯任務(wù)所涉及的代碼在選項 API 下是被分散在多個選項之中的。舉例來說,從服務(wù)端抓取一份數(shù)據(jù),可能需要用到 props
, data()
, mounted
和 watch
。極端情況下,如果我們把一個應(yīng)用中所有的邏輯任務(wù)都放在一個組件里,這個組件必然會變得龐大而難以維護,因為每個邏輯任務(wù)的代碼都被選項切成了多個碎片分散在各處。
對比之下,基于函數(shù)的 API 讓我們可以把每個邏輯任務(wù)的代碼都整理到一個對應(yīng)的函數(shù)中。當我們發(fā)現(xiàn)一個組件變得過大時,我們會將它切分成多個更小的組件;同樣地,如果一個組件的 setup()
函數(shù)變得很復(fù)雜,我們可以將它切分成多個更小的函數(shù)。而如果是基于選項,則無法做到這樣的切分,因為用 mixin 只會讓事情變得更糟糕。
從這個角度看,基于選項 vs. 基于函數(shù)就好像基于 HTML/CSS/JS 組織代碼 vs. 基于單文件組件來組織代碼。
升級策略
新的 API 和 2.x 的 API 完全兼容(只是多了一個 setup()
選項) ,并且可以一起使用。新 API 適合在組件復(fù)雜度明顯的情況下用來更好的組織和復(fù)用邏輯,或是用來提供可高度復(fù)用的插件。
理論上,新 API 可以完全提供 2.x API 的全部能力,因此我們可能會提供一個可選的編譯時選項,啟用后可以去掉所有僅為 2.x API 支持而存在的代碼,減少一部分體積和性能開銷。
附錄
與 React Hooks 的對比
這里提出的 API 和 React Hooks 有一定的相似性,具有同等的基于函數(shù)抽取和復(fù)用邏輯的能力,但也有很本質(zhì)的區(qū)別。React Hooks 在每次組件渲染時都會調(diào)用,通過隱式地將狀態(tài)掛載在當前的內(nèi)部組件節(jié)點上,在下一次渲染時根據(jù)調(diào)用順序取出。而 Vue 的 setup()
每個組件實例只會在初始化時調(diào)用一次 ,狀態(tài)通過引用儲存在 setup()
的閉包內(nèi)。這意味著基于 Vue 的函數(shù) API 的代碼:
- 整體上更符合 JavaScript 的直覺;
- 不受調(diào)用順序的限制,可以有條件地被調(diào)用;
- 不會在后續(xù)更新時不斷產(chǎn)生大量的內(nèi)聯(lián)函數(shù)而影響引擎優(yōu)化或是導(dǎo)致 GC 壓力;
- 不需要總是使用
useCallback
來緩存?zhèn)鹘o子組件的回調(diào)以防止過度更新; - 不需要擔(dān)心傳了錯誤的依賴數(shù)組給
useEffect/useMemo/useCallback
從而導(dǎo)致回調(diào)中使用了過期的值 —— Vue 的依賴追蹤是全自動的。
注:React Hooks 的開創(chuàng)性毋庸置疑,也是本提案的靈感來源。Hooks 代碼和 JSX 并置使得對值的使用更簡潔也是其優(yōu)點,但其設(shè)計確實存在上述問題,而 Vue 的響應(yīng)式系統(tǒng)恰巧能夠讓我們繞過這些問題。
Class API 的類型問題
Class API 提案的主要目的是尋找一個能夠提供更好的 TypeScript 支持的組件聲明方式。但是由于 Vue 需要將來自多個選項的屬性混合到同一個渲染上下文上,這使得即使用了 Class,要得到良好的類型推導(dǎo)也不是很容易。
以 props 的類型推導(dǎo)為例。要將 props 的類型 merge 到 class 的 this
上,我們有兩個選擇:用 class 的泛型參數(shù),或是用 decorator。
這是用泛型參數(shù)的例子:
interface Props { message: string } class App extends Component<Props> { static props = { message: String } }
由于泛型參數(shù)是純類型層面的,所以我們還需要額外地進行一次運行時的 props 選項聲明來獲得正確的行為。這就導(dǎo)致需要進行雙重聲明。
使用 decorator 的例子如下:
class App extends Component<Props> { @prop message: string }
Decorators 存在如下問題:
- ES 的 decorator 提案仍然在 stage-2 且極其不穩(wěn)定。過去一年內(nèi)已經(jīng)經(jīng)歷了兩次徹底大改,且和 TS 現(xiàn)有的實現(xiàn)已經(jīng)完全脫節(jié)?,F(xiàn)在引入一個基于 TS decorator 實現(xiàn)的 API 風(fēng)險太大。
- Decorator 只能聲明 class
this
上的屬性,卻無法將某一類 decorator 聲明的屬性歸并到一個對象上(比如$props
),這就導(dǎo)致this.$props
無法被推導(dǎo),且影響 TSX 的使用。 - 用戶很可能會覺得可以用
@prop message: string = 'foo'
這樣的寫法去聲明默認值,但事實上技術(shù)層面無法做到符合語義的實現(xiàn)。
最后,class 還有一個問題,那就是目前 class method 不支持參數(shù)的 contextual typing,也就是說我們無法基于 class 本身的 fields 來推導(dǎo)某個 method 的參數(shù)類型,需要用戶自己去聲明。
以上就是Vue開發(fā)手冊Function-based API RFC的詳細內(nèi)容,更多關(guān)于Vue Function-based API RFC的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Element控件Tree實現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼
我們在開發(fā)中肯定會遇到用樹形展示數(shù)據(jù)的需求,本文主要介紹了Element控件Tree實現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08vue3中vite的@路徑別名與path中resolve實例詳解
這篇文章主要給大家介紹了關(guān)于vue3中vite的@路徑別名與path中resolve的相關(guān)資料,文中通過實例代碼介紹的非常詳細,對大家學(xué)習(xí)或者使用vue具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2023-02-02