Redux

Redux 是一个库,它扮演着状态容器的角色,并负责管理应用的数据流。它是 Dan Abramov 在 2015 年的 ReactEurope 开发者大会上推出的 (视频)。它类似于 Flux 架构 并有很多共同点。在本章中,我们将使用 Redux 和 React 来创建一个小型的计数器应用。

Redux 架构及其主要特点

Redux architecture

类似于 Flux 架构,由视图组件 (React) 来派发动作。同一个动作也可能是由系统的其他部分派发的,例如引导逻辑。动作不是派发到中心枢纽中,而是直接派发到 store 中。注意,我们说的是 store,而不是 stores ,这是因为在 Redux 中只有一个 store ,这是 Redux 与 Flux 的最重要的区别之一。决定数据如何改变的逻辑以纯函数 ( pure functions ) 的形式存在,我们称之为 reducers 。一旦 store 接收到动作,它会将当前状态和给定动作发送给 reducer 并要求其返回一个新的状态。然后,在数据不可变的方式下, reducer 需要返回新的状态。再然后, store 更新自身的内部状态。最后,与 store 连接的 React 组件会重新渲染。

概念相当清晰并再次遵循了 单向数据流 。我们来讨论每一个部分并引入一些支持 Redux 模式工作的新术语。

动作 ( Actions )

Redux 中的动作和 Flux 一样,也只是有 type 属性的对象而已。该对象中的其他所有内容都被视为特定于上下文的数据,并且与模式无关,而与应用的逻辑相关。例如:

  1. const CHANGE_VISIBILITY = 'CHANGE_VISIBILITY';
  2. const action = {
  3. type: CHANGE_VISIBILITY,
  4. visible: false
  5. }

使用像 CHANGE_VISIBILITY 这样的常量作为动作的类型是一种最佳实践。有很多支持 Redux 的工具和库,它们用来解决不用的问题,并且都只需要动作的类型即可。所以说,动作只是传递信息的一种便捷方式。

visible 属性是我们所提到过的元数据。它与 Redux 本身无关,它表示应用中某处需要使用的数据。

每次我们想要派发动作时都需要使用这样的对象。但是,一遍又一遍地写确实是让太人烦躁了。这也正是概念 action creators 诞生的原因。action creator 是返回动作对象的函数,它可选项性地接收与动作相关联的属性。例如,如果将上面的 action 写成 action creator 会是这样:

  1. const changeVisibility = visible => ({
  2. type: CHANGE_VISIBILITY,
  3. visible
  4. });
  5. changeVisibility(false);
  6. // { type: CHANGE_VISIBILITY, visible: false }

注意,我们将 visible 的值作为参数传入,这样我们不必去记住 (或导入) 动作的确切类型。使用这种辅助函数可以让代码更紧凑,更易于阅读。

Store

Redux 提供辅助函数 createStore 来创建 store 。它的函数签名如下:

  1. import { createStore } from 'redux';
  2. createStore([reducer], [initial state], [enhancer]);

正如之前所提到的,reducer 是一个函数,它接收当前状态和动作,然后返回一个新的状态。第二个参数是 store 的初始状态。这是一种便捷的手段,可以用已有的数据来初始化我们的应用。这个功能是像服务器端渲染或持久体验这样的过程的本质。第三个参数 enhancer 提供 API 来使用第三方的中间件来扩展 Redux ,基本上是插入一些自身没有提供的功能,例如处理异步流程的工具。

创建好的 store 具有四个方法: getStatedispatchsubscribereplaceReducer 。其中最重要的或许就是 dispatch :

  1. store.dispatch(changeVisibility(false));

这里我们使用的是 action creator 。我们将其结果 (即 action 对象) 传给 dispatch 方法。然后,它会传播给应用中的 reducers 。

在典型的 React 应用中,我们通常不会直接使用 getStatesubscribe ,因为有辅助函数 (即将在后面的章节中讲到) 可以将组件和 store 联系起来并有效地订阅 store 的变化。作为订阅的一部分,我们自然可以收到当前的状态,所以不必自己去调用 getStatereplaceReducer 是一个高级 API ,它用来交互 store 所使用的当前 reducer 。我个人从来没使用过此方法。

Reducer

reducer 函数大概是 Redux 中最精华的部分。即使在此之前,我也喜欢编写纯函数来保持这种不可变的思想,但是 Redux 迫使我这样做。reducer 还有两个特点非常重要,没有它们的话基本上这种模式也不复存在。

(1) 它必须是纯函数 - 这意味着在输入不变的情况下,永远应该返回相同的结果。

(2) 它应该没有副作用 - 像访问全局变量、发起异步请求或等待 promise 解析这样的操作都不应该用在此处。

下面是个很简单的计数器 reducer :

  1. const counterReducer = function (state, action) {
  2. if (action.type === ADD) {
  3. return { value: state.value + 1 };
  4. } else if (action.type === SUBTRACT) {
  5. return { value: state.value - 1 };
  6. }
  7. return { value: 0 };
  8. };

它没有任何副作用,每次都是返回一个全新的对象。我们根据之前的状态和传入的动作类型来累加出新的值。

连接 React 组件

如果是在 React 上下文中讨论 Redux 的话,那基本离不开 react-redux 模块。它提供两样东西来进行 Redux 到组件的连接。

(1) <Provider> 组件 - 它是一个组件,它接收 store 并使得所有的子组件都可以通过 React 的 context API 来访问 store 。例如:

  1. <Provider store={ myStore }>
  2. <MyApp />
  3. </Provider>

通常,我们只在应用中的单个地方使用它。

(2) connect 函数 - 它是一个函数,它负责订阅 store 的更新和重新渲染组件。它是通过 高阶组件 实现的。这是它的函数签名:

  1. connect(
  2. [mapStateToProps],
  3. [mapDispatchToProps],
  4. [mergeProps],
  5. [options]
  6. )

mapStateToProps 参数是一个函数,它接收 store 的当前状态,并且必须返回一组键值对 (对象),这些对将作为 props 发送给我们的 React 组件。例如:

  1. const mapStateToProps = state => ({
  2. visible: state.visible
  3. });

mapDispatchToProps 也是类似的,只是它接收的是 dispatch 函数,而不是 state 。这里是我们将派发动作定义成属性的地方。

  1. const mapDispatchToProps = dispatch => ({
  2. changeVisibility: value => dispatch(changeVisibility(value))
  3. });

mergePropsmapStateToPropsmapDispatchToProps 和发送给组件的属性进行合并,它赋予我们机会去累加出更适合的属性。例如,如果我们需要触发两个动作,我们可以将它们组合成一个单独的属性并将其发送给 React 。options 接收一组如何控制连接的设置。

使用 Redux 的简单计数器应用

我们来使用上面所有的 API 来创建一个简单的计数器应用。

Redux counter app example

“Add” 和 “Subtract” 按钮只是改变 store 的值。”Visible” 和 “Hidden” 按钮用来控制计数器是否显示。

创建动作

对我来说,每个 Redux 的开始都是对动作类型建模及定义我们所要保存的状态。在这个示例中,我们会有三个操作: 增加、减少和管理可见性。所有动作代码如下所示:

  1. const ADD = 'ADD';
  2. const SUBTRACT = 'SUBTRACT';
  3. const CHANGE_VISIBILITY = 'CHANGE_VISIBILITY';
  4. const add = () => ({ type: ADD });
  5. const subtract = () => ({ type: SUBTRACT });
  6. const changeVisibility = visible => ({
  7. type: CHANGE_VISIBILITY,
  8. visible
  9. });

Store 及其 reducers

我们在解释 store 和 reudcers 时,有些技术点是没有讨论到的。通常,我们会有多个 reducer ,因为要管理多种状态。store 只有一个,所以理论上只有一个状态对象。但是大多数生产环境的应用的状态都是状态切片的组合。每个切片代表应用的一部分。这个小示例拥有计数和可见性两个切片。所以我们的初始状态应该是这样的:

  1. const initialState = {
  2. counter: {
  3. value: 0
  4. },
  5. visible: true
  6. };

我们需要为这两部分分别定义 reducer 。这样会带来灵活性并提升代码的可读性。想象一下,如果我们有一个拥有十个或更多状态切片的巨型应用,并且我们只使用单个 reducer 函数来进行维护,这样管理起来将会非常困难。

Redux 提供辅助函数来让我们能够锁定 state 的某个特定部分并为其分配一个 reducer 。它就是 combineReducers :

  1. import { createStore, combineReducers } from 'redux';
  2. const rootReducer = combineReducers({
  3. counter: function A() { ... },
  4. visible: function B() { ... }
  5. });
  6. const store = createStore(rootReducer);

函数 A 只接收 counter 切片作为状态,并且只返回切片这部分的状态。函数 B 也是同样的,它接收布尔值 (visible 的值) 并且必须返回布尔值。

counter 切片的 reducer 应该考虑到 ADDSUBTRACT 两个动作,并基于动作来计算出新的 counter 状态。

  1. const counterReducer = function (state, action) {
  2. if (action.type === ADD) {
  3. return { value: state.value + 1 };
  4. } else if (action.type === SUBTRACT) {
  5. return { value: state.value - 1 };
  6. }
  7. return state || { value: 0 };
  8. };

当 store 初始化时,每个 reducer 至少触发一次。最初运行的这一次,stateundefinedaction{ type: "@@redux/INIT"} 。在这个实例中,reducer 应该返回数据的初始值 { value: 0 }

visible 的 reducer 相当简单,它只处理动作 CHANGE_VISIBILITY :

  1. const visibilityReducer = function (state, action) {
  2. if (action.type === CHANGE_VISIBILITY) {
  3. return action.visible;
  4. }
  5. return true;
  6. };

最后,我们需要将这两个 reducers 传给 combineReducers 来创建 rootReducer

  1. const rootReducer = combineReducers({
  2. counter: counterReducer,
  3. visible: visibilityReducer
  4. });

选择器 ( Selectors )

在开始 React 组件之前,我们先介绍一个 选择器 ( selector ) 的概念。在上一节中,我们知道状态通常都是细化成多个状态切片。我们有专门的 reducer 来负责更新数据,但是当涉及到获取状态数据时,我们仍然只是有一个对象。这里就是选择器派上用场的地方。选择器就是一个函数,它接收整个状态对象并提取出我们所需要的数据。例如,在这个小示例应用中我们需要两个数据:

  1. const getCounterValue = state => state.counter.value;
  2. const getVisibility = state => state.visible;

这个计数器应用实在是太小了,完全体现不出选择器的威力。但是,在一个大项目中便截然不同。选择器的存在并不是为了少些几行代码,也不是为了可读性。选择器附带了这些内容,但它们也是上下文相关的,可能包含逻辑。由于它们可以访问整个状态,所以它们能够回答业务逻辑相关的问题。例如,“在 Y 页面用户是否有权限可以做 X 这件事”。这样的事通过一个选择器就可以完成。

React 组件

我们先来处理管理计数器可见性的 UI 部分。

  1. function Visibility({ changeVisibility }) {
  2. return (
  3. <div>
  4. <button onClick={ () => changeVisibility(true) }>
  5. Visible
  6. </button>
  7. <button onClick={ () => changeVisibility(false) }>
  8. Hidden
  9. </button>
  10. </div>
  11. );
  12. }
  13. const VisibilityConnected = connect(
  14. null,
  15. dispatch => ({
  16. changeVisibility: value => dispatch(changeVisibility(value))
  17. })
  18. )(Visibility);

第二个组件略微有些复杂。将它命名为 Counter ,它渲染两个按钮和计数值。

  1. function Counter({ value, add, subtract }) {
  2. return (
  3. <div>
  4. <p>Value: { value }</p>
  5. <button onClick={ add }>Add</button>
  6. <button onClick={ subtract }>Subtract</button>
  7. </div>
  8. );
  9. }
  10. const CounterConnected = connect(
  11. state => ({
  12. value: getCounterValue(state)
  13. }),
  14. dispatch => ({
  15. add: () => dispatch(add()),
  16. subtract: () => dispatch(subtract())
  17. })
  18. )(Counter);

这里 mapStateToPropsmapDispatchToProps 都需要,因为我们想读取 store 中的数据,同时还要派发动作。这个组件要接收三个属性: valueaddsubtract

最后要完成的就是 App 组件,我们在这里进行应用的组装。

  1. function App({ visible }) {
  2. return (
  3. <div>
  4. <VisibilityConnected />
  5. { visible && <CounterConnected /> }
  6. </div>
  7. );
  8. }
  9. const AppConnected = connect(
  10. state => ({
  11. visible: getVisibility(state)
  12. })
  13. )(App);

我们再一次需要对组件进行 connect 操作,因为我们想要控制计数器的显示。getVisibility 选择器返回布尔值,它表示是否渲染 CounterConnected 组件。

结语

Redux 是一种很棒的模式。最近几年,JavaScript 社区将这种理念发扬下去,并使用一些新术语对其进行了增强。我认为一个典型的 Redux 应用应该是下面这样的:

Redux architecture

顺便一提,我们还没有介绍过副作用管理。那将是另外的新篇章了,它有自己的理念和解决方案。

我们可以得出结论,Redux 本身是一种非常简单的模式。它传授了非常有用的技术,但不幸的是光靠它自身往往是不够的。我们迟早要引入更多的概念或模式。当然这没有那么糟糕,我们只是先提起计划一下。