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