Redux 是一個很是經典的狀態管理庫,在 2019 年接近年末的時候這個項目用 TypeScript 重寫了。網上有不少分析 Redux JavaScript 代碼實現的文章,然而 TypeScript 部分的卻不多。我在看重寫的 TypeScript 代碼時發現有不少地方比較有意思,也啓發我提煉了一些東西,因此整理成了這篇博客,歡迎一塊兒來討論和學習。html
本文內容分紅兩個部分,第一部分是關於 Redux 中類型定義和推導的技巧,這部分徹底是 TypeScript 代碼和相關概念,若是不熟悉 TypeScript 的話基本是無法看,能夠找官方文檔補課後再來;第二部分是我提煉的一些我的心得,包括我理解的 Redux 設計思路,咱們從中怎麼學習和應用等等,這部分只要知道函數式編程思想就行了。前端
Redux 把全部的類型定義都放在 types 文件夾中。主要描述了 Redux 中的抽象定義,好比什麼是 Action
和 Reducer
;還有一部分是推導類型,好比:ActionFromReducer
、StateFromReducersMapObject
等等。git
我列了幾個比較有意思的來一塊兒康康。github
export type ReducerFromReducersMapObject<M> = M extends { [P in keyof M]: infer R } ? R extends Reducer<any, any> ? R : never : never
這個推導類型的目的是從 ReducersMapObject
中推導出 Reducer
的類型。這裏有個知識點:在映射類型中,infer
會推導出聯合類型。請看下面的例子:typescript
export type ValueType<M> = M extends { [P in keyof M]: infer R } ? R : never type Person = { name: string; age: number; address: string; } type T1 = ValueType<Person>; // T1 = string | number
export type ExtendState<State, Extension> = [Extension] extends [never] ? State : State & Extension
這個類型是用來推導擴展 State 的。若是沒有擴展,就返回 State 自己,不然返回 State 和 Extension 的交叉類型。這裏比較奇怪的是爲何判斷 never
要用 [Extension] extends [never]
而不是 Extension extends never
呢?編程
代碼註釋中很貼心的有一個此問題的討論連接:https://github.com/microsoft/...。大概意思是有人寫了個推導類型,可是行爲不符合指望因此提了個 issue:數組
type MakesSense = never extends never ? 'yes' : 'no' // Resolves to 'yes' type ExtendsNever<T> = T extends never ? 'yes' : 'no' type MakesSenseToo = ExtendsNever<{}> // Resolves to 'no' type Huh = ExtendsNever<never> // Expect to resolve to 'yes', actually resolves to never
咱們注意到他用了 Extension extends never
。但當泛型參數傳入 never
時,結果不是 yes
而是 never
!閉包
下面有人給出了答案:app
This is the expected behavior,
ExtendsNever
is a distributive conditional type. Conditional types distribute over unions. Basically ifT
is a unionExtendsNever
is applied to each member of the union and the result is the union of all applications (ExtendsNever<'a' | 'b'> == ExtendsNever<'a' > | ExtendsNever<'b'>
).never
is the empty union (ie a union with no members). This is hinted at by its behavior in a union'a' | never == 'a'
. So when distributing overnever
,ExtendsNever
is never applied, since there are no members in this union and thus the result is never.框架If you disable distribution for
ExtendsNever
, you get the behavior you expect:type MakesSense = never extends never ? 'yes' : 'no' // Resolves to 'yes' type ExtendsNever<T> = [T] extends [never] ? 'yes' : 'no' type MakesSenseToo = ExtendsNever<{}> // Resolves to 'no' type Huh = ExtendsNever<never> // is yes
我結合官方文檔來講一下爲何這個行爲是符合預期的。由於 ExtendsNever
在這裏是分發的條件類型:Distributive conditional types。分發的條件類型在實例化時會自動分發成聯合類型。 例如,實例化 T extends U ? X : Y
,T
的類型爲 A | B | C
,會被解析爲 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
。
而當實例化的泛型爲 never
時,ExtendsNever
不會執行,由於聯合類型是 never
至關於沒有聯合類型成員,因此上面的結果是根本不會進入條件判斷而直接返回 never
。因此要解決這個問題須要的就是打破分發的條件類型,使其不要分發。
官方文檔上寫了分發的條件類型的觸發條件:若是待檢查的類型是naked type parameter
。那什麼是 naked type
呢?簡單點來講就是沒有被其餘類型包裹的類型,其餘類型包括:數組、元組、或其餘泛型等。這裏我也找了一個 stack overflow 上面的解答你們能夠參考一下。
看到這裏問題就迎刃而解了:[Extension] extends [never]
把 never
包裹成元組就是爲了打破分發的條件類型以實現正確地判斷是否 never
。
declare const $CombinedState: unique symbol export type CombinedState<S> = { readonly [$CombinedState]?: undefined } & S
這個類型是用來區分 State 是否由 combineReducers
建立的,combineReducers
函數會返回這個類型。咱們知道 TypeScript 的對象類型兼容是結構子類型,也就是說只要對象的結構知足就行了。而 combineReducers
構造的 State 又須要與普通的 State 對象區分開來,這個時候就須要一個標識的屬性來檢查不一樣——$CombinedState
。
首先注意到的是,$CombinedState
是一個 unique symbol
,這說明這個 symbol 的類型是惟一的,TypeScript 能夠追蹤和識別它的類型;而後咱們看到 $CombinedState
是 declare
出來的而且沒有導出,這代表 $CombinedState
只用來作類型定義用的(不須要實現)而且不能被外部的類型僞造。
接着看下面 CombinedState<S>
裏面的 { readonly [$CombinedState]?: undefined }
部分。[$CombinedState]
屬性是可選的而且類型是 undefined
並且不能被賦值修改,這就說明這個對象裏面啥也沒有嘛。而後與 S
作交叉,保持了看起來與 S
的「結構同樣」(S
類型的變量能夠賦值給 CombinedState<S>
類型的變量),但又被完美地從結構類型上區分開了,這個玩法有點高級!
來看下面的測試好好體會一下:
type T1 = { a: number; b: string; } declare const $CombinedState: unique symbol; type T2<T> = { readonly [$CombinedState]?: undefined } & T; type T3<T> = {} & T; type T4<T> = Required<T> extends { [$CombinedState]: undefined } ? 'yes' : 'no'; type S1 = T2<T1>; // type S1 = { readonly [$CombinedState]?: undefined; } & T1; type S2 = T4<S1>; // type S2 = "yes"; type S3 = T3<T1>; // type S3 = T1; type S4 = T4<S3>; // type S4 = "no"; let s: S1 = { a: 1, b: '2' };
export type PreloadedState<S> = Required<S> extends { [$CombinedState]: undefined } ? S extends CombinedState<infer S1> ? { [K in keyof S1]?: S1[K] extends object ? PreloadedState<S1[K]> : S1[K] } : never : { [K in keyof S]: S[K] extends string | number | boolean | symbol ? S[K] : PreloadedState<S[K]> }
PreloadedState
是調用 createStore
時 State 預設值的類型,只有 Combined State 的屬性值纔是可選的。類型的推導方案藉助了上面的 CombinedState
來完成,而且是一個遞歸的推導。這裏我有個疑問是爲何判斷是否原始類型 primitive 的方式上下不一致?
以上是我以爲 Redux 類型定義中比較有意思的地方,其餘的類型定義內容應該比較好理解你們能夠多康康,若是有疑問也能夠提出來一塊兒討論。
接着是第二部分的內容,我我的對於 Redux 設計思想與實現的心得理解,還有一些觀點和建議。
提及來我用 Redux 已經好久了。2016 年決定把主要精力放在前端時是學習的 React,接觸的第一個狀態管理框架就是 Redux,而且如今公司的前端業務層也是圍繞着 Redux 技術棧打造的。我很早就看過 Redux 的 JavaScript 代碼,加上 TypeScript 的代碼部分能夠說我對 Redux 已經很熟悉了,因此此次決定要好好總結一下。
我認爲 Redux 具備很是學院派的函數式編程思想,若是你想編寫一個功能庫給別人使用,徹底可使用 Redux 的思想當作模板來應用。爲何我會這麼說呢,來看下如下幾點。
思考一個問題:爲什麼 Redux 不用 Class
來實現,是編寫習慣嗎?
因爲 JavaScript 目前的語言特性,Class
產生的對象沒法直接隱藏數據屬性,在運行時健壯性有缺陷。仔細看看 Redux 的實現方式:createStore
函數返回一個對象,在 createStore
函數內部存放數據變量,返回的對象只暴露了方法,這就是典型的利用閉包隱藏數據,是咱們經常使用的函數式編程思想之一。
在設計一個給別人使用的功能庫時,咱們首先要考慮的問題是什麼?我認爲是能提供什麼樣的功能,換句話說就是功能庫的行爲是怎樣的。咱們來看看 Redux 是怎麼考慮這個問題的,在 types\store.ts
中有一個 Store
接口(我把註釋都去掉了):
export interface Store< S = any, A extends Action = AnyAction, StateExt = never, Ext = {} > { dispatch: Dispatch<A> getState(): S subscribe(listener: () => void): Unsubscribe replaceReducer<NewState, NewActions extends Action>( nextReducer: Reducer<NewState, NewActions> ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext [Symbol.observable](): Observable<S> }
這個接口定義就是 createStore
返回的對象類型定義。從定義能夠看出這個對象提供了幾個方法,這就是 Redux 提供給用戶使用的主要行爲了。
行爲是一種契約,用戶將按照你給出的行爲來使用你提供的功能。在函數式編程中,函數就是行爲,因此咱們要重點關注行爲的設計。而且行爲的變動通常來講代價很大,會形成不兼容,因此在函數式編程中咱們必定要學習如何去抽象行爲。
Redux 是如何擴展功能的,咱們可能會聯想到 Redux middleware。可是你仔細想一想,中間件的設計就表明了 Redux 的功能擴展嗎?
直接說結論:中間件擴展的是 dispatch
的行爲,不是 Redux 自己。爲了解決 action
在派送過程當中的異步、特殊業務處理等各類場景需求,Redux 設計了中間件模式。但中間件僅表明這個特殊場景的擴展需求,這個需求是高頻的,因此 Redux 專門實現了這個模式。
在 types\store.ts
中有一個 StoreEnhancer
的定義,這個纔是 Redux 擴展的設計思路:
export type StoreEnhancer<Ext = {}, StateExt = never> = ( next: StoreEnhancerStoreCreator<Ext, StateExt> ) => StoreEnhancerStoreCreator<Ext, StateExt> export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = < S = any, A extends Action = AnyAction >( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> ) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
不難看出 StoreEnhancer
是一個高階函數,經過傳入原來的 createStore
函數而返回一個新的 createStore
函數來實現擴展。Store & Ext
在保留原有行爲的基礎上實現了擴展,因此高階函數是經常使用的擴展功能的方式。對於使用者來講, 編寫擴展時也要遵照 里氏替換原則。
let currentListeners: (() => void)[] | null = [] let nextListeners = currentListeners /** * This makes a shallow copy of currentListeners so we can use * nextListeners as a temporary list while dispatching. */ function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } function subscribe(listener: () => void) { // ... ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { // ... ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) currentListeners = null } } function dispatch(action: A) { // ... try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }
以上就是 Redux「訂閱/發佈」的關鍵代碼了,我說兩點能夠借鑑學習的地方。
subscribe
中返回 unsubscribe
原來我剛開始寫「訂閱/發佈」模式時,會把「取消訂閱」寫成一個獨立的函數 囧。把 subscrible
寫成一個高階函數,返回 unsubscribe
,這樣對於使用者來講能夠更方便地使用匿名函數來接收通知。
currentListeners
和 nextListeners
能夠保證在發佈時通知是穩定的。由於可能在發佈通知期間有新的訂閱者或者退訂的狀況,那麼在這種狀況下這一次的發佈過程是穩定的不會受影響,變化始終在 nextListeners
。
當咱們去看源碼學習一個項目時,不要只看一次就完了。應隔一段時間去重溫一下,從不一樣維度、不一樣視角去觀察,多想一想,多問幾個爲何,提煉出本身的心得,那就真的是學到了。
歡迎 star 和關注個人 JS 博客:小聲比比 JavaScript