从 Redux 设计理念到源码分析

关注并将「趣谈前端」设为星标

每早08:30按时推送技术干货/优秀开源/技术思维

前言

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

话说回来,都 2021 年了还在写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。

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

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

// 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
// 定义的一个函数,接受 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>
}

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

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
// A extends Action
dispatch({ type: ActionTypes.INIT } as A)

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

return
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
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        `我 reducer 正在执行,newState 正在产出呢!现在不行`
      )
    }

    return currentState as S
  }

方法很简单,就是return currentState

subscribe

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

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

使用如下:

const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

unsubscribe();
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 一份。

  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

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

dispatch
  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
  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中文文档


10款2021年国外顶尖的lowcode开发平台

2个小时, 从学到做, 我用Dooring制作了3个电商H5

前端:使用纯css实现超实用的图标库(附源码)


点个在看你最好看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值