技巧與思惟可兼得——讀 TypeScript of Redux 有感

Redux 是一個很是經典的狀態管理庫,在 2019 年接近年末的時候這個項目用 TypeScript 重寫了。網上有不少分析 Redux JavaScript 代碼實現的文章,然而 TypeScript 部分的卻不多。我在看重寫的 TypeScript 代碼時發現有不少地方比較有意思,也啓發我提煉了一些東西,因此整理成了這篇博客,歡迎一塊兒來討論和學習。html

本文內容分紅兩個部分,第一部分是關於 Redux 中類型定義和推導的技巧,這部分徹底是 TypeScript 代碼和相關概念,若是不熟悉 TypeScript 的話基本是無法看,能夠找官方文檔補課後再來;第二部分是我提煉的一些我的心得,包括我理解的 Redux 設計思路,咱們從中怎麼學習和應用等等,這部分只要知道函數式編程思想就行了。前端

Types

Redux 把全部的類型定義都放在 types 文件夾中。主要描述了 Redux 中的抽象定義,好比什麼是 ActionReducer;還有一部分是推導類型,好比:ActionFromReducerStateFromReducersMapObject 等等。git

我列了幾個比較有意思的來一塊兒康康。github

ReducerFromReducersMapObject

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

ExtendState

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 if T is a union ExtendsNever 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 over never, 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 : YT 的類型爲 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

CombinedState

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 能夠追蹤和識別它的類型;而後咱們看到 $CombinedStatedeclare 出來的而且沒有導出,這代表 $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' };

PreloadedState

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 好在哪裏?

提及來我用 Redux 已經好久了。2016 年決定把主要精力放在前端時是學習的 React,接觸的第一個狀態管理框架就是 Redux,而且如今公司的前端業務層也是圍繞着 Redux 技術棧打造的。我很早就看過 Redux 的 JavaScript 代碼,加上 TypeScript 的代碼部分能夠說我對 Redux 已經很熟悉了,因此此次決定要好好總結一下。

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 的功能擴展嗎?

Redux 中間件的本質

直接說結論:中間件擴展的是 dispatch 的行爲,不是 Redux 自己。爲了解決 action 在派送過程當中的異步、特殊業務處理等各類場景需求,Redux 設計了中間件模式。但中間件僅表明這個特殊場景的擴展需求,這個需求是高頻的,因此 Redux 專門實現了這個模式。

Redux 擴展:StoreEnhancer

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「訂閱/發佈」的關鍵代碼了,我說兩點能夠借鑑學習的地方。

  1. subscribe 中返回 unsubscribe

    原來我剛開始寫「訂閱/發佈」模式時,會把「取消訂閱」寫成一個獨立的函數 囧。把 subscrible 寫成一個高階函數,返回 unsubscribe,這樣對於使用者來講能夠更方便地使用匿名函數來接收通知。

  2. 穩定的發佈鏈

    currentListenersnextListeners 能夠保證在發佈時通知是穩定的。由於可能在發佈通知期間有新的訂閱者或者退訂的狀況,那麼在這種狀況下這一次的發佈過程是穩定的不會受影響,變化始終在 nextListeners

總結

當咱們去看源碼學習一個項目時,不要只看一次就完了。應隔一段時間去重溫一下,從不一樣維度、不一樣視角去觀察,多想一想,多問幾個爲何,提煉出本身的心得,那就真的是學到了。

歡迎 star 和關注個人 JS 博客:小聲比比 JavaScript

相關文章
相關標籤/搜索