Redux 源码解析

什么是状态?

状态(State)是指应用程序在某个时刻的内部状态。状态可以包括应用程序的当前数据、用户输入、用户操作等。状态通常由一个对象或变量来表示,它可以帮助开发人员管理应用程序的行为和响应用户输入的变化。例如,在处理用户输入时,开发人员需要将用户输入的状态转换为应用程序可以使用的格式。

为什么使用Redux?

现实问题

随着前端应用开发日趋复杂,需要管理的状态越来越多,状态变化越来越频繁,我们很容易就对这些状态何时发生、为什么发生以及怎么发生的失去控制;组件之间的通信和协调也变得困难。尤其是跨越多个层级的组件。因此,如何更好地进行状态管理是理我们需要思考的问题。

状态管理库

为了应对日益复杂的 State 状态,行业内提出不同的解决方案,目前比较常见的状态管理库有redux、zustand、xstate、mobx、jotai、recoil等。从 npm trends 看各个状态管理库近一年的下载量趋势:

可以看到 Redux 作为 React 状态管理的老大哥,下载量上依然瑶瑶领先其他小兄弟。那么Redux有什么魔力使得自己成为绝大多数开发者状态管理库的首选。

Redux的优点

  • 可预测性,严格遵循单向数据流,所有的状态变更都通过分发 action,使得状态变更变得可预测,容易理解和调试。可预测性是 Reudx 设计的最大亮点;
  • 繁荣的社区,拥有庞大的社区和成熟的支持,以及大量的教程、实例;
  • 可扩展性高,中间件模式提供了灵活的扩展能力,让你可以随心所欲的武装你的 dispatch;
  • 良好的开发体验,拥有成熟的开发调试工具 redux devtools;

Redux能带来什么

Redux 帮你管理"全局"状态,提供的模式使你更容易理解应用程序中的状态何时、何地、为什么、state如何被更新,以及当这些更改发生时你的应用程序逻辑将如何更新。指导你编写可预测和可测试的代码。

如果我们的页面特别复杂,如上左图所示,存在父子组件,子父组件,兄弟组件之间的通信,数据也存在正向,单向,跨层的数据流动,在未使用任何状态管理库之前,维护起来就会非常困难;在使用 Redux 状态管理库之后,如上右图所示,所有组件间的通信都通过分发 action,一次一个 action,针对应用内唯一的 store 进行更改,然后再通过订阅更新组件的UI,所有的数据流向都是单向的,确保整个流程更加清晰。

认识Flux

Redux 基于 Flux 单向数据流的思想,在深入了解 Redux 之前我们有必要先了解下 Flux。

Flux 是由 Facebook 提出的一种应用架构模式,作为主流的JavaScript MVC模式的替代品被开发。传统MVC模式中多采用双向数据绑定,视图的改变会更新相应的模型,模型的更改也会更新相应的视图,模型(model)、视图(view)和控制器(controller)之间的数据流可能会难以理解追寻。Flux试图解决状态的不可预测性,以及模型与视图紧密耦合的架构的脆弱性。废弃了双向数据绑定模型,转而采用单项数据流。Flux要求对状态的所有修改遵循单一的路径,而不允许每个视图与对应的模型进行交互。

在 Flux 架构中,一个应用将被拆分为以下 4 个部分

  • View:用户界面。

  • Action:可以理解为视图层发出的"消息",它会触发应用状态的改变

  • Dispatcher:派发器,负责对 action 进行分发;

  • Store:数据层,存储应用状态的"仓库",此外还会定义修改状态的逻辑。store 的变化最终会映射到 view 层上去

Flux工作流如下图所示:

用户与 View 之间产生交互,通过 View 发起一个 action,Dispatcher 会把这个 Action 派发给 Store,通知 Store 进行相应的状态更新。Store 状态更新完成后,会进一步通知 View 去更新界面。

Redux 的介绍

Redux 是JavaScrip应用程序的可预测状态的容器。作为React的状态管理层,主要目标是为应用程序中的数据带来一致性与可预测性。Redux将状态管理的职责划分为一下几个独立的单元:

  • Store:将应用程序的所有状态都存储在单个对象中(对象树)

  • Action:描述实现的对象,只能使用action更新store

  • Reducer:指定了如何转换应用程序的状态,它接收store中的当前状态和一个action,然后返回更新后的下一个状态

Redux工作流如下图所示:

三大原则

Redux中的状态由单一的可信数据源(single source of truth)表示,是只读的,并且只能用纯函数进行修改。这其中蕴含了Redux的三大原则,即

  • 单一数据源:整个应用的 state 被存储在一个对象树中,并且只存在于唯一的 store 中;
  • 状态是只读的:不能直接去修改 state,只能通过触发 action 来返回一个新的 state;
  • 改变由纯函数进行:要使用纯函数来修改 state;

Redux与Flux的区别

Redux 是一种基于 Flux 思想的实现方式,它在Flux基础上进行了改进。Redux采用了单向数据流的概念,与Flux最大的区别在于 Redux 只有一个 Store,而 Flux 允许多个 Store 存在。相较于Flux,Redux的单一Store更加清晰和易于管理。在Flux中,会有多个Store用于存储应用数据,并在Store中执行更新逻辑,当Store发生变化时,再通知controller-view来更新数据。而Redux将这些Store整合成一个完整的Store,并且可以通过这个Store推导出整个应用的State。

另外,Redux的更新逻辑不再放在Store中执行,而是放在Reducer中。单一Store带来的好处是,所有的数据结果都集中在一处,操作起来更加便利。只要将它传递给最外层组件,内层组件就不需要维持自己的State,只需要通过props从父级组件传递下来即可。这使得子组件变得异常简单。

Redux使用示例

我们先来看下Redux的示例代码

javascript 复制代码
import { createStore } from 'redux';
// store 至少需要一个 reducer 函数(counterReducer)
function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
  return state;
}
// 使用 reducer 创建 store
const store = createStore(counterReducer);
// 读取 store 的当前状态
console.log(store.getState());
// store订阅一个更新函数,待dispatch之后,执行这个更新函数,获取新的值
store.subscribe(() -> {
  console.log('current state: ', store.getState());
});
const render = () => ReactDOM.render(
    <div>
      <span>{store.getState()}</span>
      // 向 reducer 发送一个 action 来更新 store
      <button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button>
      <button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button>
    </div>,
    root
)
render()

这个例子演示了如何使用Redux来实现点击按钮增减数字的效果。

  • 首先,我们使用 reducer 创建一个 store,以便与 Redux 进行交互。

  • 通过调用store.getState()方法获取当前的数字,初始值为0。

  • 使用store.subscribe()方法订阅一个用于更新页面的函数。每当reducer返回新的值时,该函数就会被调用

  • 当点击按钮时,我们通过调用dispatch方法告诉Redux我们是要增加还是减少数字(同时传入相应的action)。

  • dispatch方法内部会调用我们定义的reducer函数,结合当前的state和action,返回新的state。

  • 在返回新的state之后,我们调用订阅的更新函数,以更新页面。

总结一下,我们所有的操作都是通过store进行的,通过createStore方法创建唯一的 store ,提供getSate、dispatch、subscribe等方法。

Redux 的实现

现在让我们来看一下Redux内部的实现逻辑。Redux源码的src目录,主要包含6个核心文件,主要包括index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js文件

  • index.js:Redux入口文件,暴露出来主要接口
  • createStore.js:会创建一个 store 及其相应的 getSate、dispatch 和 subscribe 操作
  • combineReducers.js:合并多个 reducer 为一个总的 reducer
  • bindActionCreators.js:返回包裹 dispatch 的函数可以直接使用。 一般用在 mapDispatchToProps 里
  • applyMiddleware.js:提供中间件,如 redux-thunk、redux-logger
  • compose.js:combineReducers 中会用到的工具函数

下面我们就逐一对核心源码实现进行解读

createStore(reducer)

createStore 函数是Redux的核心,接受reducer状态更新函数,最终返回对象store,用来存放应用中的state,应用中仅有一个store,包含dispatch、subscribe、getState、replaceReducer等方法。简化后的代码如下

javascript 复制代码
export function createStore(reducer, preloadedState, enhancer) {
  // 省略异常处理

  // 当前的reducer
  let currentReducer = reducer
  // 当前的state
  let currentState = preloadedState
  let currentListeners: Map<number, ListenerCallback> | null = new Map()
  let nextListeners = currentListeners
  let listenerIdCounter = 0
  // 是否正在dispatch aciton,想当于一把锁
  let isDispatching = false

  // 对当前订阅进行浅拷贝,防止在dispatch时订阅/取消订阅产生bug
  function ensureCanMutateNextListeners() {}

  // 读取由store管理的状态树
  function getState(): S {}

  // 注册监听事件,维护存放所有订阅函数的数组,并返回取消订阅的函数
  function subscribe(listener: () => void) {}

  // 分发action,这是触发State状态树变化的唯一方式
  function dispatch(action: A) {}

  // 替换当前store的reducer,并重新初始化State状态树
  function replaceReducer(nextReducer: Reducer<S, A>): void {}

  function observable() {}

  // store被创建后,分发'INIT'的action,初始State状态树
  dispatch({ type: ActionTypes.INIT } as A)

  // 组装并返回store对象
  const store = {
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown as Store<S, A, StateExt> & Ext
  return store
}

store.dispatch(action)

dispatch函数分发action,是修改state的唯一方式

javascript 复制代码
function dispatch(action) {
// 省略异常处理
    
    try {
      isDispatching = true
      // 调用当前reducer,传入当前state和action,并更新state状态树
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
        
    const listeners = (currentListeners = nextListeners)
    // 依次执行注册的lister监听事件
    listeners.forEach(listener => {
      listener()
    })
    // 返回action
    return action
}

store.getState()

getState函数比较简单,直接返回当前状态树

csharp 复制代码
  function getState() {
    // 省略异常处理
    
    // 返回当前状态树
    return currentState as S
  }

store.replaceReducer(nextReducer)

replaceReducer函数用于替换当前store对象的reducer

php 复制代码
  function replaceReducer(nextReducer) {
    // 省略异常处理
    
        // 替换当前reducer
    currentReducer = nextReducer

    // 分发'REPLACE'的action,重新初始化State状态树
    dispatch({ type: ActionTypes.REPLACE } as A)
  }

store.subscribe(lister)

subscribe接收监听函数,主要做了两件事,一是订阅监听函数,存放在数组中,在store.dispatch(action)时遍历执行监听函数,二是返回取消订阅函数

csharp 复制代码
  function subscribe(listener: () => void) {
    // 省略异常处理

    let isSubscribed = true

    ensureCanMutateNextListeners()
    // 自增订阅函数id
    const listenerId = listenerIdCounter++
    // 将订阅函数放入store内部维护的集合中
    nextListeners.set(listenerId, listener)

    // 返回取消订阅的函数
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      nextListeners.delete(listenerId)
      currentListeners = null
    }
  }

combineReducers()

combineReducers 函数用于将多个reducer函数合并成一个总的reducer,每次Reducer返回新的state时会和老的state对比,如果发生改变,hasChanged为true,使用新state触发页面更新;反之则不做处理。

typescript 复制代码
export default function combineReducers(reducers) {
  // 获取所有reducers的key,组成数组
  const reducerKeys = Object.keys(reducers)
  // 获取最终有效的reducers,组成字典finalReducers
  const finalReducers: { [key: string]: Reducer<any, any, any> } = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache: { [key: string]: true }
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError: unknown
  try {
    // 用于检查每个reducer有没有默认返回的state
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 这个函数的核心是循环遍历finalReducers,获取每个reducer对应的旧状态,并根据当前dispatch的action,生成新的sate状态,最终将所有新的state做为值,reducer的name为键,生成做最终的state
  return function combination(
    state: StateFromReducersMapObject<typeof reducers> = {},
    action: Action
  ) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    // 是否state发生了变化
    let hasChanged = false
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      // 获取每个reducer的name
      const key = finalReducerKeys[i]
      // 获取每个reducer
      const reducer = finalReducers[key]
      // 获取每个reducer的旧state状态
      const previousStateForKey = state[key]
      // 获取每个reducer,根据这个reducer的旧state状态和当前action生成新state状态
      const nextStateForKey = reducer(previousStateForKey, action)
      // 以每个reducer的name为key,新state状态为值,更新最终的state
      nextState[key] = nextStateForKey
      // 判断是否state的值发生了变化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    // state发生了变化,返回新的sate,否则返回旧的sate
    return hasChanged ? nextState : state
  }
}

bindActionCreator()

bindActionCreator函数是将单个actionCreator绑定到dispatch上,bindActionCreators就是将多个actionCreators绑定到dispatch上。

javascript 复制代码
function bindActionCreator(actionCreator, dispatch) {
  return function (this, ...args) {
    return dispatch(actionCreator.apply(this, args))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 省略异常处理

  const boundActionCreators: ActionCreatorsMapObject = {}
  // 遍历整个actionCreators,如果item是function类型,就调用bindActionCreator将actionCreator绑定到dispatch上
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

applyMiddleware()

applyMiddleware是一个增强器,组合多个中间件,最终增强store.dispatch()函数。reducer的设计理念是纯函数,如果需要一些副作用(处理异步逻辑、日志记录等),中间件的作用就体现出来了。

javascript 复制代码
export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  return createStore => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    // 省略异常处理

    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    // 遍历中间件,将middlewareAPI传进去,得到的chain是一个数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 利用compose函数依次执行中间件函数得到加强后的dispatch
    dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

总结

本文主要介绍了Redux的设计理念和源码解析。正如任何工具都有适用与非适用的场景,在使用Redux时也需要做权衡,Reudx的创始人之一Dan Abramov也发表过"你可能不需要Redux(You Might Not Need Redux)"。Redux适用于应用程序中存在大量状态,且状态变更逻辑复杂频繁的场景,对于没有复杂状态的较小程序,使用Redux产生的模板代码,反而使得不如直接使用React来的方便。

hi, 我是快手社交的 键盘破风手

快手社交技术团队 正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们 , 一起创造世界级的产品~

热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理, 测试开发... 大量 HC 等你来呦~ 内部推荐请发简历至 >>>我们的邮箱: social-tech-team@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘

相关推荐
WeiShuai9 分钟前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife14 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu15 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill18 分钟前
nestjs使用ESM模块化
前端
加油吧x青年36 分钟前
Web端开启直播技术方案分享
前端·webrtc·直播
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白1 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧2 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog2 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川2 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试