技巧与思惟可兼得——读 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

相关文章
相关标签/搜索