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