Reudx 源码解析

什么是状态?

状态(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 <<<, 备注我的花名成功率更高哦~ 😘

相关推荐
你挚爱的强哥1 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js