共计 7372 个字符,预计需要花费 19 分钟才能阅读完成。
Redux 是一个十分经典的状态治理库,在 2019 年靠近年底的时候这个我的项目用 TypeScript 重写了。网上有很多剖析 Redux JavaScript 代码实现的文章,然而 TypeScript 局部的却很少。我在看重写的 TypeScript 代码时发现有很多中央比拟有意思,也启发我提炼了一些货色,所以整顿成了这篇博客,欢送一起来探讨和学习。
本文内容分成两个局部,第一局部是对于 Redux 中类型定义和推导的技巧,这部分齐全是 TypeScript 代码和相干概念,如果不相熟 TypeScript 的话根本是没法看,能够找官网文档补课后再来;第二局部是我提炼的一些集体心得,包含我了解的 Redux 设计思路,咱们从中怎么学习和利用等等,这部分只有晓得函数式编程思维就好了。
Types
Redux 把所有的类型定义都放在 types 文件夹中。次要形容了 Redux 中的形象定义,比方什么是 Action
和 Reducer
;还有一部分是推导类型,比方:ActionFromReducer
、StateFromReducersMapObject
等等。
我列了几个比拟有意思的来一起康康。
ReducerFromReducersMapObject
export type ReducerFromReducersMapObject<M> = M extends {[P in keyof M]: infer R
}
? R extends Reducer<any, any>
? R
: never
: never
这个推导类型的目标是从 ReducersMapObject
中推导出 Reducer
的类型。这里有个知识点:在映射类型中,infer
会推导出联结类型。请看上面的例子:
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
!
上面有人给出了答案:
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
。
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 能够追踪和辨认它的类型;而后咱们看到 $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'};
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“订阅 / 公布”的要害代码了,我说两点能够借鉴学习的中央。
- 从
subscribe
中返回unsubscribe
原来我刚开始写“订阅 / 公布”模式时,会把“勾销订阅”写成一个独立的函数 囧。把
subscrible
写成一个高阶函数,返回unsubscribe
,这样对于使用者来说能够更不便地应用匿名函数来接管告诉。 - 稳固的公布链
currentListeners
和nextListeners
能够保障在公布时告诉是稳固的。因为可能在公布告诉期间有新的订阅者或者退订的状况,那么在这种状况下这一次的公布过程是稳固的不会受影响,变动始终在nextListeners
。
总结
当咱们去看源码学习一个我的项目时,不要只看一次就完了。应隔一段时间去重温一下,从不同维度、不同视角去察看,多想想,多问几个为什么,提炼出本人的心得,那就真的是学到了。
欢送 star 和关注我的 JS 博客:小声比比 JavaScript