从 Redux 设计理念到源码分析

2020-04-26 10:49:31 浏览数 (1)

前言

Redux 也是我列在 THE LAST TIME 系列中的一篇,由于现在正在着手探究关于我目前正在开发的业务中状态管理的方案。所以,这里打算先从 Redux 中学习学习,从他的状态中取取经。毕竟,成功总是需要站在巨人的肩膀上不是。

话说回来,都 2020 年了还在写 Redux 的文章,真的是有些过时了。不过呢,当时 Redux 孵化过程中一定也是回头看了 FluxCQRSES 等。

本篇先从 Redux 的设计理念到部分源码分析。下一篇我们在注重说下 ReduxMiddleware工作机制。至于手写,推荐砖家大佬的:完全理解 redux(从零实现一个 redux)

Redux

Redux 并不是什么特别 Giao 的技术,但是其理念真的提的特别好。

说透了,它就是一个提供了 settergetter 的大闭包,。外加一个 pubSub。。。另外的什么 reducermiddleware 还是 action什么的,都是基于他的规则和解决用户使用痛点而来的,仅此而已。下面我们一点点说。。。

设计思想

在 jQuery 时代的时候,我们是「面向过程开发」,随着 react 的普及,我们提出了状态驱动 UI 的开发模式。我们认为:「Web 应用就是状态与 UI 一一对应的关系」

但是随着我们的 web 应用日趋的复杂化,一个应用所对应的背后的 state 也变的越来越难以管理。

Redux 就是我们 Web 应用的一个状态管理方案」

一一对应

如上图所示,store 就是 Redux 提供的一个状态容器。里面存储着 View 层所需要的所有的状态(state)。每一个 UI 都对应着背后的一个状态。Redux 也同样规定。一个 state 就对应一个 View。只要 state 相同,View 就相同。(其实就是 state 驱动 UI)。

为什么要使用 Redux

如上所说,我们现在是状态驱动 UI,那么为什么需要 Redux 来管理状态呢?react 本身就是 state drive view 不是。

原因还是由于现在的前端的地位已经愈发的不一样啦,前端的复杂性也是越来越高。通常一个前端应用都存在大量复杂、无规律的交互。还伴随着各种异步操作。

任何一个操作都可能会改变 state,那么就会导致我们应用的 state 越来越乱,且被动原因愈发的模糊。我们很容易就对这些状态何时发生、为什么发生、怎么发生而失去控制。

如上,如果我们的页面足够复杂,那么view 背后state 的变化就可能呈现出这个样子。不同的 component 之间存在着父子、兄弟、子父、甚至跨层级之间的通信。

而我们理想中的状态管理应该是这个样子的:

单纯的从架构层面而言,UI 与状态完全分离,并且单向的数据流确保了状态可控。

Redux 就是做这个的!

  • 每一个 State 的变化可预测
  • 动作和状态统一管理

下面简单介绍下 Redux 中的几个概念。其实初学者往往就是对其概念而困惑。

store

❝保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。 ❞

State

❝某一个时刻,存储着的应用状态值 ❞

Action

❝View 发出的一种让 state 发生变化的通知 ❞

Action Creator

❝可以理解为 Action 的工厂函数 ❞

dispatch

❝View 发出 Action 的媒介。也是唯一途径 ❞

reducer

❝根据当前接收到的ActionState,整合出来一个全新的 State。注意是需要是纯函数 ❞

三大原则

Redux 的使用,基于以下三个原则

单一数据源

单一数据源这或许是与 Flux 最大的不同了。在 Redux 中,整个应用的 state 都被存储到一个object 中。当然,这也是唯一存储应用状态的地方。我们可以理解为就是一个 Object tree。不同的枝干对应不同的 Component。但是归根结底只有一个根。

也是受益于单一的 state tree。以前难以实现的“撤销/重做”甚至回放。都变得轻松了很多。

State 只读

唯一改变 state 的方法就是 dispatch 一个 actionaction 就是一个令牌而已。normal Object

任何 state 的变更,都可以理解为非 View 层引起的(网络请求、用户点击等)。View 层只是发出了某一种意图。而如何去满足,完全取决于 Redux 本身,也就是 reducer。

代码语言:javascript复制
store.dispatch({
  type:'FETCH_START',
  params:{
    itemId:233333
  }
})
使用纯函数来修改

所谓纯函数,就是你得纯,别变来变去了。书面词汇这里就不做过多解释了。而这里我们说的纯函数来修改,其实就是我们上面说的 reducer

Reducer 就是纯函数,它接受当前的 stateaction。然后返回一个新的 state。所以这里,state 不会更新,只会替换。

之所以要纯函数,就是结果可预测性。只要传入的 stateaction 一直,那么就可以理解为返回的新 state 也总是一样的。

总结

Redux 的东西远不止上面说的那么些。其实还有比如 middleware、actionCreator 等等等。其实都是使用过程中的衍生品而已。我们主要是理解其思想。然后再去源码中学习如何使用。

源码分析

❝Redux 源码本身非常简单,限于篇幅,我们下一篇再去介绍composecombineReducersapplyMiddleware

目录结构

Redux 源码本身就是很简单,代码量也不大。学习它,也主要是为了学习他的编程思想和设计范式。

当然,我们也可以从 Redux 的代码里,看看大佬是如何使用 ts 的。所以源码分析里面,我们还会去花费不少精力看下 Redux 的类型说明。所以我们从 type 开始看

src/types

看类型声明也是为了学习Redux 的 ts 类型声明写法。所以相似声明的写法形式我们就不重复介绍了。

actions.ts

类型声明也没有太多的需要去说的逻辑,所以我就写注释上吧

代码语言:javascript复制
// Action的接口定义。type 字段明确声明
export interface Action<T = any> {
  type: T
}
export interface AnyAction extends Action {
  // 在 Action 的这个接口上额外扩展的另外一些任意字段(我们一般写的都是 AnyAction 类型,用一个“基类”去约束必须带有 type 字段)
  [extraProps: string]: any
}
export interface ActionCreator<A> {
  // 函数接口,泛型约束函数的返回都是 A
  (...args: any[]): A
}
export interface ActionCreatorsMapObject<A = any> {
  // 对象,对象值为 ActionCreator
  [key: string]: ActionCreator<A>
}
reducers.ts
代码语言:javascript复制
// 定义的一个函数,接受 S 和继承 Action 默认为 AnyAction 的 A,返回 S
export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S

// 可以理解为 S 的 key 作为ReducersMapObject的 key,然后 value 是  Reducer的函数。in 我们可以理解为遍历
export type ReducersMapObject<S = any, A extends Action = Action> = {
  [K in keyof S]: Reducer<S[K], A>
}

上面两个声明比较简单直接。下面两个稍微麻烦一些

代码语言:javascript复制
export type StateFromReducersMapObject<M> = M extends ReducersMapObject<
  any,
  any
>
  ? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }
  : never
  
export type ReducerFromReducersMapObject<M> = M extends {
  [P in keyof M]: infer R
}
  ? R extends Reducer<any, any>
    ? R
    : never
  : never

上面两个声明,咱们来解释其中第一个吧(稍微麻烦些)。

  • StateFromReducersMapObject 添加另一个泛型M约束
  • M 如果继承 ReducersMapObject<any,any>则走{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }的逻辑
  • 否则就是 never。啥也不是
  • { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never } 很明显,这就是一个对象,key 来自 M 对象里面,也就是ReducersMapObject里面传入的Skey 对应的 value 就是需要判断 M[P]是否继承自 Reducer。否则也啥也不是
  • infer 关键字和 extends 一直配合使用。这里就是指返回 Reducer 的这个 State 「的类型」
其他

types 目录里面其他的比如 storemiddleware都是如上的这种声明方式,就不再赘述了,感兴趣的可以翻阅翻阅。然后取其精华的应用到自己的 ts 项目里面

src/createStore.ts

不要疑惑上面函数重载的写法~

可以看到,整个createStore.ts 就是一个createStore 函数。

createStore

三个参数:

  • reducer:就是 reducer,根据 action 和 currentState 计算 newState 的纯 Function
  • preloadedState:initial State
  • enhancer:增强store的功能,让它拥有第三方的功能

createStore 里面就是一些「闭包函数的功能整合」

INIT
代码语言:javascript复制
// A extends Action
dispatch({ type: ActionTypes.INIT } as A)

这个方法是Redux保留用的,用来初始化State,其实就是dispatch 走到我们默认的 switch case default 的分支里面获取到默认的 State

return
代码语言:javascript复制
const store = ({
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

ts 的类型转换语法就不说了,返回的对象里面包含dispatchsubscribegetStatereplaceReducer[$$observable].

这里我们简单介绍下前三个方法的实现。

getState
代码语言:javascript复制
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        `我 reducer 正在执行,newState 正在产出呢!现在不行`
      )
    }

    return currentState as S
  }

方法很简单,就是 return currentState

subscribe

subscribe的作用就是添加监听函数listener,让其在每次dispatch action的时候调用。

返回一个移除这个监听的函数。

使用如下:

代码语言:javascript复制
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

unsubscribe();
代码语言:javascript复制
function subscribe(listener: () => void) {
    // 如果 listenter 不是一个 function,我就报错(其实 ts 静态检查能检查出来的,但!那是编译时,这是运行时)
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 同 getState 一个样纸
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. '  
        'If you would like to be notified after the store has been updated, subscribe from a '  
        'component and invoke store.getState() in the callback to access the latest state. '  
        'See https://`Redux`.js.org/api/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    // 直接将监听的函数放进nextListeners里
    nextListeners.push(listener)

    return function unsubscribe() {// 也是利用闭包,查看是否以订阅,然后移除订阅
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. '  
          'See https://`Redux`.js.org/api/store#subscribelistener for more details.'
        )
      }

      isSubscribed = false//修改这个订阅状态

      ensureCanMutateNextListeners()
      //找到位置,移除监听
      const index = nextListeners.indexOf(listener) 
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

「一句话解释就是在 listeners 数据里面添加一个函数」

再来说说这里面的ensureCanMutateNextListeners,很多 Redux 源码都么有怎么提及这个方法的作用。也是让我有点困惑。

这个方法的实现非常简单。就是判断当前的监听数组里面是否和下一个数组相等。如果是!则 copy 一份。

代码语言:javascript复制
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

那么为什么呢?这里留个彩蛋。等看完 dispatch 再来看这个疑惑。

dispatch
代码语言:javascript复制
  function dispatch(action: A) {
  // action必须是个普通对象
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. '  
        'Use custom middleware for async actions.'
      )
    }
  // 必须包含 type 字段
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. '  
        'Have you misspelled a constant?'
      )
    }
  // 同上
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 设置正在 dispatch 的 tag 为 true(解释了那些判断都是从哪里来的了)
      isDispatching = true
      // 通过传入的 reducer 来去的新的 state
      //  let currentReducer = reducer
      currentState = currentReducer(currentState, action)
    } finally {
    // 修改状态
      isDispatching = false
    }
    
    // 将 nextListener 赋值给 currentListeners、listeners (注意回顾 ensureCanMutateNextListeners )
    const listeners = (currentListeners = nextListeners)
    // 挨个触发监听
    for (let i = 0; i < listeners.length; i  ) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

方法很简单,都写在注释里了。这里我们再回过头来看ensureCanMutateNextListeners的意义

ensureCanMutateNextListeners
代码语言:javascript复制
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  
  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) {
    // ... 
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i  ) {
      const listener = listeners[i]
      listener()
    }
    // ...
    return action
  }

从上,代码看起来貌似只要一个数组来存储listener 就可以了。但是事实是,我们恰恰就是我们的 listener 是可以被 unSubscribe 的。而且 slice 会改变原数组大小。

所以这里增加了一个 listener 的副本,是为了避免在遍历listeners的过程中由于subscribe或者unsubscribelisteners进行的修改而引起的某个listener被漏掉了。

最后

限于篇幅,就暂时写到这吧~

其实后面打算重点介绍的 Middleware,只是中间件的一种更规范,甚至我们可以理解为,它并不属于 Redux 的。因为到这里,你已经完全可以自己写一份状态管理方案了。

combineReducers也是我认为是费巧妙的设计。所以这些篇幅,就放到下一篇吧~

参考链接

  • redux
  • 10行代码看尽Redux实现
  • Redux 中文文档

0 人点赞