支援 Options API
Options API
到目前為止,我們已經能夠使用 Composition API 實作很多功能,但讓我們也支援 Options API。
在本書中,我們在 Options API 中支援以下內容:
- props
- data
- computed
- method
- watch
- slot
- lifecycle
- onMounted
- onUpdated
- onUnmounted
- onBeforeMount
- onBeforeUpdate
- onBeforeUnmount
- provide/inject
- $el
- $data
- $props
- $slots
- $parent
- $emit
- $forceUpdate
- $nextTick
作為實作方法,讓我們在 "componentOptions.ts" 中準備一個名為 "applyOptions" 的函式,並在 "setupComponent" 的末尾執行它。
export const setupComponent = (instance: ComponentInternalInstance) => {
// .
// .
// .
if (render) {
instance.render = render as InternalRenderFunction
}
// ↑ 這是現有實作
setCurrentInstance(instance)
applyOptions(instance)
unsetCurrentInstance()
}在 Options API 中,開發者介面經常處理 "this"。
const App = defineComponent({
data() {
return { message: 'hello' }
},
methods: {
greet() {
console.log(this.message) // 像這樣
},
},
})在內部,"this" 在 Options API 中指向元件的代理,在應用選項時,這個代理被綁定。
實作示例 ↓
export function applyOptions(instance: ComponentInternalInstance) {
const { type: options } = instance
const publicThis = instance.proxy! as any
const ctx = instance.ctx
const { methods } = options
if (methods) {
for (const key in methods) {
const methodHandler = methods[key]
if (isFunction(methodHandler)) {
ctx[key] = methodHandler.bind(publicThis)
}
}
}
}基本上,如果你使用這個原理逐一實作它們,應該不會太困難。
如果你想讓 "data" 變成響應式,在這裡呼叫 "reactive" 函式,如果你想計算,在這裡呼叫 "computed" 函式。("provide/inject" 也是如此)
由於在執行 "applyOptions" 之前透過 "setCurrentInstance" 設定了實例,你可以以相同的方式呼叫到目前為止一直使用的 API(Composition API)。
關於以 "$" 開頭的屬性,它們由 "componentPublicInstance" 的實作控制。"PublicInstanceProxyHandlers" 中的 getter 控制它們。
為 Options API 加入型別
在功能上,按照上述描述實作是可以的,但為 Options API 加入型別有點複雜。
在本書中,我們為 Options API 支援基本型別。
困難的地方在於 "this" 的型別根據使用者對每個選項的定義而改變。例如,如果你在 "data" 選項中定義一個名為 "count" 的 "number" 型別屬性,你希望 "computed" 或 "method" 中的 "this" 推斷出 "count: number"。
當然,這不僅適用於 "data",也適用於在 "computed" 或 "methods" 中定義的那些。
const App = defineComponent({
data() {
return { count: 0 }
},
methods: {
myMethod() {
this.count // number
this.myComputed // number
},
},
computed: {
myComputed() {
return this.count // number
},
},
})為了實作這一點,你需要實作一個有些複雜的型別拼圖(具有許多泛型的中繼)。
從 "defineComponent" 的型別開始,我們實作幾種型別來中繼到 "ComponentOptions" 和 "ComponentPublicInstance"。
在這裡,讓我們專注於 "data" 和 "methods" 進行解釋。
首先,通常的 "ComponentOptions" 型別。我們用泛型擴充它以接受 "data" 和 "methods" 的型別作為參數,"D" 和 "M"。
export type ComponentOptions<
D = {},
M extends MethodOptions = MethodOptions
> = {
data?: () => D;,
methods?: M;
};
interface MethodOptions {
[key: string]: Function;
}到目前為止,應該不會太困難。這是可以應用於 "defineComponent" 參數的型別。 當然,在 "defineComponent" 中,我們也接受 "D" 和 "M" 來中繼使用者定義的型別。這允許我們中繼使用者定義的型別。
export function defineComponent<
D = {},
M extends MethodOptions = MethodOptions,
>(options: ComponentOptions<D, M>) {}問題是在 "methods" 中處理 "this" 時如何將 "D" 與 "this" 混合(如何使 "this.count" 這樣的推斷成為可能)。
首先,"D" 和 "M" 被合併到 "ComponentPublicInstance" 中(合併到代理中)。這可以理解如下(用泛型擴充)。
type ComponentPublicInstance<
D = {},
M extends MethodOptions = MethodOptions,
> = {
/** 公開實例具有的各種型別 */
} & D &
M一旦我們有了這個,我們就將實例型別混合到 "ComponentOptions" 中的 "this" 中。
type ComponentOptions<D = {}, M extends MethodOptions = MethodOptions> = {
data?: () => D
methods?: M
} & ThisType<ComponentPublicInstance<D, M>>透過這樣做,我們可以從選項中的 "this" 推斷在 "data" 和 "method" 中定義的屬性。
在實踐中,我們需要推斷各種型別,如 "props","computed" 和 "inject",但基本原理是相同的。 乍一看,你可能會被許多泛型和型別轉換(如從 "inject" 中僅提取 "key")所壓倒,但如果你冷靜下來並回到原理,你應該沒問題。 在本書的程式碼中,受原始 Vue 的啟發,我們用 "CreateComponentPublicInstance" 進一步抽象了一步,並實作了一個名為 "ComponentPublicInstanceConstructor" 的型別,但不要太擔心。(如果你感興趣,你也可以閱讀它!)
到此為止的原始碼: chibivue (GitHub)
