做者:崔靜javascript
defineComponent 自己的功能很簡單,可是最主要的功能是爲了 ts 下的類型推到。對於一個 ts 文件,若是咱們直接寫html
export default {}
複製代碼
這個時候,對於編輯器而言,{} 只是一個 Object 的類型,沒法有針對性的提示咱們對於 vue 組件來講 {} 裏應該有哪些屬性。可是增長一層 defineComponet 的話,vue
export default defineComponent({})
複製代碼
這時,{} 就變成了 defineComponent 的參數,那麼對參數類型的提示,就能夠實現對 {} 中屬性的提示,外還能夠進行對參數的一些類型推導等操做。java
可是上面的例子,若是你在 vscode 的用 .vue 文件中嘗試的話,會發現不寫 defineComponent 也同樣有提示。這個實際上是 Vetur 插件進行了處理。git
下面看 defineComponent 的實現,有4個重載,先看最簡單的第一個,這裏先不關心 DefineComponent 是什麼,後面細看。github
// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props, RawBindings = object>( setup: ( props: Readonly<Props>, ctx: SetupContext ) => RawBindings | RenderFunction ): DefineComponent<Props, RawBindings> 複製代碼
defineComponet 參數爲 function, function 有兩個參數 props 和 ctx,返回值類型爲 RawBindings 或者 RenderFunction。defineComponet 的返回值類型爲 DefineComponent<Props, RawBindings>
。這其中有兩個泛型 Props 和 RawBindings。Props 會根據咱們實際寫的時候給 setup 第一個參數傳入的類型而肯定,RawBindings 則根據咱們 setup 返回值來肯定。一大段話比較繞,寫一個相似的簡單的例子來看:typescript
相似 props 用法的簡易 demo 以下,咱們給 a 傳入不一樣類型的參數,define 返回值的類型也不一樣。這種叫 Generic Functions數組
declare function define<Props>(a: Props): Props const arg1:string = '123' const result1 = define(arg1) // result1:string const arg2:number = 1 const result2 = define(arg2) // result2: number 複製代碼
相似 RawBindings 的簡易 demo以下: setup 返回值類型不一樣,define 返回值的類型也不一樣markdown
declare function define<T>(setup: ()=>T): T const arg1:string = '123' const resul1 = define(() => { return arg1 }) const arg2:number = 1 const result2 = define(() => { return arg2 }) 複製代碼
由上面兩個簡易的 demo,能夠理解重載1的意思,defineComponet 返回類型爲DefineComponent<Props, RawBindings>
,其中 Props 爲 setup 第一個參數類型;RawBindings 爲 setup 返回值類型,若是咱們返回值爲函數的時候,取默認值 object。從中能夠掌握一個 ts 推導的基本用法,對於下面的定義dom
declare function define<T>(a: T): T 複製代碼
能夠根據運行時傳入的參數,來動態決定 T 的類型 這種方式也是運行時類型和 typescript 靜態類型的惟一聯繫,不少咱們想經過運行時傳入參數類型,來決定其餘相關類型的時候,就可使用這種方式。
接着看 definComponent,它的重載2,3,4分別是爲了處理 options 中 props 的不一樣類型。看最多見的 object 類型的 props 的聲明
export function defineComponent< // the Readonly constraint allows TS to treat the type of { required: true }
// as constant instead of boolean.
PropsOptions extends Readonly<ComponentPropsOptions>,
RawBindings,
D,
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>,
EE extends string = string
>(
options: ComponentOptionsWithObjectProps<
PropsOptions,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE
>
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
複製代碼
和上面重載1差很少的思想,核心思想也是根據運行時寫的 options 中的內容推導出各類泛型。在 vue3 中 setup 的第一個參數是 props,這個 props 的類型須要和咱們在 options 傳入的一致。這個就是在ComponentOptionsWithObjectProps
中實現的。代碼以下
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>>,
Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Defaults
> & {
props: PropsOptions & ThisType<void>
} & ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false
>
>
export interface ComponentOptionsBase<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
Defaults = {}
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: ( this: void, props: Props, ctx: SetupContext<E, Props> ) => Promise<RawBindings> | RawBindings | RenderFunction | void
//...
}
複製代碼
很長一段,一樣的先用一個簡化版的 demo 來理解一下:
type TypeA<T1, T2, T3> = {
a: T1,
b: T2,
c: T3
}
declare function define<T1, T2, T3>(options: TypeA<T1, T2, T3>): T1 const result = define({ a: '1', b: 1, c: {} }) // result: string 複製代碼
根據傳入的 options 參數 ts 會推斷出 T1,T2,T3的類型。獲得 T1, T2, T3 以後,能夠利用他們進行其餘的推斷。稍微改動一下上面的 demo,假設 c 是一個函數,裏面的參數類型由 a 的類型來決定:
type TypeA<T1, T2, T3> = TypeB<T1, T2>
type TypeB<T1, T2> = {
a: T1
b: T2,
c: (arg:T1)=>{}
}
const result = define({
a: '1',
b: 1,
c: (arg) => { // arg 這裏就被會推導爲一個 string 的類型
return arg
}
})
複製代碼
而後來看 vue 中的代碼,首先 defineComponent
能夠推導出 PropsOptions。可是 props 若是是對象類型的話,寫法以下
props: {
name: {
type: String,
//... 其餘的屬性
}
}
複製代碼
而 setup 中的 props 參數,則須要從中提取出 type 這個類型。因此在 ComponentOptionsWithObjectProps 中
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
//...
Props = Readonly<ExtractPropTypes<PropsOptions>>,
//...
>
複製代碼
經過 ExtracPropTypes 對 PropsOptions 進行轉化,而後獲得 Props,再傳入 ComponentOptionsBase,在這個裏面,做爲 setup 參數的類型
export interface ComponentOptionsBase<
Props,
//...
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: ( this: void, props: Props, ctx: SetupContext<E, Props> ) => Promise<RawBindings> | RawBindings | RenderFunction | void
複製代碼
這樣就實現了對 props 的推導。
this 的做用
在 setup 定義中第一個是 this:void 。咱們在 setup 函數中寫邏輯的時候,會發現若是使用了 this.xxx
IDE 中會有錯誤提示
Property 'xxx' does not exist on type 'void'
這裏經過設置 this:void
來避免咱們在 setup 中使用 this。
this 在 js 中是一個比較特殊的存在,它是根據運行上上下文決定的,因此 typescript 中有時候沒法準確的推導出咱們代碼中使用的 this 是什麼類型的,因此 this 就變成了 any,各類類型提示/推導啥的,也都沒法使用了(注意:只有開啓了 noImplicitThis 配置, ts 纔會對 this 的類型進行推導)。爲了解決這個問題,typescript 中 function 的能夠明確的寫一個 this 參數,例如官網的例子:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function (this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
複製代碼
明確的定義出在 createCardPicker 中的 this 是 Deck 的類型。這樣在 createCardPicker
中 this 下可以使用的屬性/方法,就被限定爲 Deck 中的。
另外和 this 有關的,還有一個 ThisType
。
上面提到了,咱們寫的 props
{
props: {
name1: {
type: String,
require: true
},
name2: {
type: Number
}
}
}
複製代碼
通過 defineComponent 的推導以後,會被轉換爲 ts 的類型
ReadOnly<{
name1: string,
name2?: number | undefined
}>
複製代碼
這個過程就是利用 ExtractPropTypes 實現的。
export type ExtractPropTypes<O> = O extends object
? { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
{ [K in OptionalKeys<O>]?: InferPropType<O[K]> }
: { [K in string]: any }
複製代碼
根據類型中清晰的命名,很好理解:利用 RrequiredKeys<O>
和 OptionsKeys<O>
將 O 按照是否有 required 進行拆分(之前面props爲例子)
{
name1
} & {
name2?
}
複製代碼
而後每一組裏,用 InferPropType<O[K]>
推導出類型。
InferPropType
在理解這個以前,先理解一些簡單的推導。首先咱們在代碼中寫
props = {
type: String
}
複製代碼
的話,通過 ts 的推導,props.type 的類型是 StringConstructor。因此第一步須要從 StringConstructor/ NumberConstructor 等 xxConstrucror 中獲得對應的類型 string/number 等。能夠經過 infer 來實現
type a = StringConstructor
type ConstructorToType<T> = T extends { (): infer V } ? V : never
type c = ConstructorToType<a> // type c = String
複製代碼
上面咱們經過 ():infer V
來獲取到類型。之因此能夠這樣用,和 String/Number 等類型的實現有關。javascript 中能夠寫
const key = String('a')
複製代碼
此時,key 是一個 string 的類型。還能夠看一下 StringConstructor 接口類型表示
interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}
複製代碼
上面有一個 ():string
,因此經過 extends {(): infer V}
推斷出來的 V 就是 string。
而後再進一步,將上面的 a 修改爲 propsOptions 中的內容,而後把 ConstructorToType 中的 infer V 提到外面一層來判斷
type a = StringConstructor
type ConstructorType<T> = { (): T }
type b = a extends {
type: ConstructorType<infer V>
required?: boolean
} ? V : never // type b = String
複製代碼
這樣就簡單實現了將 props 中的內容轉化爲 type 中的類型。
由於 props 的 type 支持不少中寫法,vue3 中實際的代碼實現要比較複雜
type InferPropType<T> = T extends null
? any // null & true would fail to infer
: T extends { type: null | true }
? any
// As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
// 這裏單獨判斷了 ObjectConstructor 和 BooleanConstructor
: T extends ObjectConstructor | { type: ObjectConstructor }
? Record<string, any>
: T extends BooleanConstructor | { type: BooleanConstructor }
? boolean
: T extends Prop<infer V, infer D> ? (unknown extends V ? D : V) : T
// 支持 PropOptions 和 PropType 兩種形式
type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null
required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown): boolean
}
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
type PropConstructor<T = any> =
| { new (...args: any[]): T & object } // 能夠匹配到其餘的 Constructor
| { (): T } // 能夠匹配到 StringConstructor/NumberConstructor 和 () => string 等
| PropMethod<T> // 匹配到 type: (a: number, b: string) => string 等 Function 的形式
// 對於 Function 的形式,經過 PropMethod 構形成了一個和 stringConstructor 類型的類型
// PropMethod 做爲 PropType 類型之一
// 咱們寫 type: Function as PropType<(a: string) => {b: string}> 的時候,就會被轉化爲
// type: (new (...args: any[]) => ((a: number, b: string) => {
// a: boolean;
// }) & object) | (() => (a: number, b: string) => {
// a: boolean;
// }) | {
// (): (a: number, b: string) => {
// a: boolean;
// };
// new (): any;
// readonly prototype: any;
// }
// 而後在 InferPropType 中就能夠推斷出 (a:number,b:string)=> {a: boolean}
type PropMethod<T, TConstructor = any> =
T extends (...args: any) => any // if is function with args
? {
new (): TConstructor;
(): T;
readonly prototype: TConstructor
} // Create Function like constructor
: never
複製代碼
RequiredKeys
這個用來從 props 中分離出必定會有值的 key,源碼以下
type RequiredKeys<T> = {
[K in keyof T]: T[K] extends
| { required: true }
| { default: any }
// don't mark Boolean props as undefined
| BooleanConstructor
| { type: BooleanConstructor }
? K
: never
}[keyof T]
複製代碼
除了明肯定義 reqruied 之外,還包含有 default 值,或者 boolean 類型。由於對於 boolean 來講若是咱們不傳入,就默認爲 false;而有 default 值的 prop,必定不會是 undefined
OptionalKeys
有了 RequiredKeys, OptionsKeys 就很簡單了:排除了 RequiredKeys 便可
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
複製代碼
ExtractDefaultPropTypes 和 ExtractPropTypes 相似,就不寫了。
推導 options 中的 method,computed, data 返回值, 都和上面推導 props 相似。
vue3 的 options 中新增長了一個 emits 配置,能夠顯示的配置咱們在組件中要派發的事件。配置在 emits 中的事件,在咱們寫 $emit
的時候,會做爲函數的第一個參數進行提示。
對獲取 emits 中配置值的方式和上面獲取 props 中的類型是相似的。$emit
的提示,則是經過 ThisType
來實現的(關於 ThisType 參考另一篇文章介紹)。下面是簡化的 demo
declare function define<T>(props:{ emits: T, method?: {[key: string]: (...arg: any) => any} } & ThisType<{ $emits: (arg: T) => void }>):T const result = define({ emits: { key: '123' }, method: { fn() { this.$emits(/*這裏會提示:arg: { key: string; }*/) } } }) 複製代碼
上面會推導出 T 爲 emits 中的類型。而後 & ThisType
,使得在 method 中就可使用 this.$emit
。再將 T 做爲 $emit 的參數類型,就能夠在寫 this.$emit
的時候進行提示了。
而後看 vue3 中的實現
export function defineComponent< //... 省卻其餘的 E extends EmitsOptions = Record<string, any>, //... >( options: ComponentOptionsWithObjectProps< //... E, //... > ): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE> export type ComponentOptionsWithObjectProps< //.. E extends EmitsOptions = EmitsOptions, //... > = ComponentOptionsBase< // 定義一個 E 的泛型 //... E, //... > & {
props: PropsOptions & ThisType<void>
} & ThisType<
CreateComponentPublicInstance< // 利用 ThisType 實現 $emit 中的提示
//...
E,
//...
>
>
// ComponentOptionsWithObjectProps 中 包含了 ComponentOptionsBase
export interface ComponentOptionsBase<
//...
E extends EmitsOptions, // type EmitsOptions = Record<string, ((...args: any[]) => any) | null> | string[]
EE extends string = string,
Defaults = {}
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
//..
emits?: (E | EE[]) & ThisType<void> // 推斷出 E 的類型
}
export type ComponentPublicInstance<
//...
E extends EmitsOptions = {},
//...
> = {
//...
$emit: EmitFn<E> // EmitFn 來提取出 E 中的 key
//...
}
複製代碼
在一邊學習一邊實踐的時候踩到一個坑。踩坑過程:將 emits 的推導過程實現了一下
export type ObjectEmitsOptions = Record<
string,
((...args: any[]) => any) | null
>
export type EmitsOptions = ObjectEmitsOptions | string[];
declare function define<E extends EmitsOptions = Record<string, any>, EE extends string = string>(options: E| EE[]): (E | EE[]) & ThisType<void> 複製代碼
而後用下面的方式來驗證結果
const emit = ['key1', 'key2']
const a = define(emit)
複製代碼
看 ts 提示的時候發現,a 的類型是 const b: string[] & ThisType<void>
,可是實際中 vue3 中寫一樣數組的話,提示是 const a: (("key1" | "key2")[] & ThisType<void>) | (("key1" | "key2")[] & ThisType<void>)
糾結很久,最終發現寫法的不一樣:用下面寫法的話推導出來結果一致
define(['key1', 'key2'])
複製代碼
可是用以前的寫法,經過變量傳入的時候,ts 在拿到 emit 時候,就已經將其類型推導成了 string[]
,因此 define 函數中拿到的類型就變成了 string[]
,而不是原始的 ['key1', 'key2']
所以須要注意:在 vue3 中定義 emits 的時候,建議直接寫在 emits 中寫,不要提取爲單獨的變量再傳給 emits
真的要放在單獨變量裏的話,須要進行處理,使得 '[key1', 'key2']
的變量定義返回類型爲 ['key1', 'key2']
而非 string[]
。可使用下面兩種方式:
方式一
const keys = ["key1", "key2"] as const; // const keys: readonly ["key1", "key2"]
複製代碼
這種方式寫起來比較簡單。可是有一個弊端,keys 爲轉爲 readonly 了,後期沒法對 keys 進行修改。
方式二
type UnionToIntersection<T> = (T extends any ? (v: T) => void : never) extends (v: infer V) => void ? V : never
type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
type Push<T extends any[], V> = [ ...T, V]
type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = N extends true ? [] : Push<UnionToTuple<Exclude<T, L>>, L>
declare function tuple<T extends string>(arr: T[]): UnionToTuple<T> const c = tuple(['key1', 'key2']) // const c: ["key1", "key2"] 複製代碼
首先經過 arr: T[]
將 ['key1', 'key2']
轉爲 union,而後經過遞歸的方式, LastOf
獲取 union 中的最後一個,Push
到數組中。
vue3 中寫在 mixins 或 extends 中的內容能夠在 this
中進行提示。對於 mixins 和 extends 來講,與上面其餘類型的推斷有一個很大的區別:遞歸。因此在進行類型判斷的時候,也須要進行遞歸處理。舉個簡單的例子,以下
const AComp = {
methods: {
someA(){}
}
}
const BComp = {
mixins: [AComp],
methods: {
someB() {}
}
}
const CComp = {
mixins: [BComp],
methods: {
someC() {}
}
}
複製代碼
對於 CComp 中的 this 的提示,應該有方法 someB 和 someA。爲了實現這個提示,在進行類型推斷的時候,須要一個相似下面的 ThisType
ThisType<{
someA
} & {
someB
} & {
someC
}>
複製代碼
因此對於 mixins 的處理,就須要遞歸獲取 component 中的 mixins 中的內容,而後將嵌套的類型轉化爲扁平化的,經過 & 來連接。看源碼中實現:
// 判斷 T 中是否有 mixin
// 若是 T 含有 mixin 那麼這裏結果爲 false,覺得 {mixin: any} {mixin?: any} 是沒法互相 extends 的
type IsDefaultMixinComponent<T> = T extends ComponentOptionsMixin
? ComponentOptionsMixin extends T ? true : false
: false
//
type IntersectionMixin<T> = IsDefaultMixinComponent<T> extends true
? OptionTypesType<{}, {}, {}, {}, {}> // T 不包含 mixin,那麼遞歸結束,返回 {}
: UnionToIntersection<ExtractMixin<T>> // 獲取 T 中 Mixin 的內容進行遞歸
// ExtractMixin(map type) is used to resolve circularly references
type ExtractMixin<T> = {
Mixin: MixinToOptionTypes<T>
}[T extends ComponentOptionsMixin ? 'Mixin' : never]
// 經過 infer 獲取到 T 中 Mixin, 而後遞歸調用 IntersectionMixin<Mixin>
type MixinToOptionTypes<T> = T extends ComponentOptionsBase<
infer P,
infer B,
infer D,
infer C,
infer M,
infer Mixin,
infer Extends,
any,
any,
infer Defaults
>
? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
IntersectionMixin<Mixin> &
IntersectionMixin<Extends>
: never
複製代碼
extends 和 mixin 的過程相同。而後看 ThisType 中的處理
ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false
>
>
export type CreateComponentPublicInstance<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
// 將嵌套的結構轉爲扁平化的
PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
// 提取 props
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
// 提取 RawBindings,也就是 setup 返回的內容
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
// 提取 data 返回的內容
PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
EnsureNonVoid<C>,
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>
> = ComponentPublicInstance< // 上面結果傳給 ComponentPublicInstance,生成 this context 中的內容
PublicP,
PublicB,
PublicD,
PublicC,
PublicM,
E,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>
>
複製代碼
以上就是總體大部分的 defineComponent 的實現,能夠看出,他純粹是爲了類型推導而生的,同時,這裏邊用到了不少不少類型推導的技巧,還有一些這裏沒有涉及,感興趣的同窗能夠去仔細看下 Vue 中的實現。