乐趣区

关于javascript:TypeScript-of-Redux-心得体会

Redux 是一个十分经典的状态治理库,在 2019 年靠近年底的时候这个我的项目用 TypeScript 重写了。网上有很多剖析 Redux JavaScript 代码实现的文章,然而 TypeScript 局部的却很少。我在看重写的 TypeScript 代码时发现有很多中央比拟有意思,也启发我提炼了一些货色,所以整顿成了这篇博客,欢送一起来探讨和学习。

本文内容分成两个局部,第一局部是对于 Redux 中类型定义和推导的技巧,这部分齐全是 TypeScript 代码和相干概念,如果不相熟 TypeScript 的话根本是没法看,能够找官网文档补课后再来;第二局部是我提炼的一些集体心得,包含我了解的 Redux 设计思路,咱们从中怎么学习和利用等等,这部分只有晓得函数式编程思维就好了。

Types

Redux 把所有的类型定义都放在 types 文件夹中。次要形容了 Redux 中的形象定义,比方什么是 ActionReducer;还有一部分是推导类型,比方:ActionFromReducerStateFromReducersMapObject 等等。

我列了几个比拟有意思的来一起康康。

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 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

退出移动版