选项式 API 的支持
Options API
到目前为止已经可以用 Composition 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 在内部指向组件实例的 proxy 代理对象,并在应用选项(applyOptions)时绑定此 proxy。
实现如下 ↓
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)
}
}
}
}基本上我们都可以按照这种方式一个一个实现 Options API 中的所有内容。
如果您想使 data 中的数据变成响应式的,您可以在这里调用 reactive 函数,如果您想使用计算属性选项,您可以在这里调用computed 函数(provide/inject 也是一样的)。
由于 setCurrentInstance 在运行 applyOptions 之前设置了组件实例,因此可以像往常一样调用以前实现的 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 类型。
现在我们将扩展这个类型,并使用泛型参数 D 和 M 来接收 data 和 methods 的类型。
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>) {}问题是如何将 D 与 methods 中的 this 混合(即我们该如何实现 this.count 这类数据的类型推理)。
首先,D 和 M 会被合并到 PendentPublicInstance 中(合并到代理中)。
我们可以这么理解(使用泛型进行扩展):
type ComponentPublicInstance<
D = {},
M extends MethodOptions = MethodOptions,
> = {
/** public instance 原本拥有的各种数据类型 */
} & D &
Mここまでできたら、ComponentOptions の this にインスタンスの型を混ぜ込みます。
type ComponentOptions<D = {}, M extends MethodOptions = MethodOptions> = {
data?: () => D
methods?: M
} & ThisType<ComponentPublicInstance<D, M>>这样,我们可以从 option 中的 this 推论出 data 和 method 中定义的属性与类型。
在后面的实现中,我们还需要实现 props、computed 和 inject 的类型推断,但是原理都是差不多的。
乍一看,你可能会因为有许多泛型和类型转换(例如从 inject 中提取出 key)而感到困惑,但只要冷静下来,回归到基础原理然后实现,应该就没问题了。
在本书的代码中,受到 Vue.js 源代码的启发,我们引入了一个抽象层 CreateComponentPublicInstance,并实现了一个名为 ComponentPublicInstanceConstructor 的类型,但请不必太在意这些细节。(如果感兴趣的话,也可以看看那部分内容!)
当前源代码位于: chibivue (GitHub)
The chibivue Book