要点

资源:
Redux文档:详解
Redux文档中文版:浅析

应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers

  1. import { createStore } from 'redux';
  2. /** 这是一个 reducer,形式为 (state, action) => state 的纯函数。
  3. * 描述了 action 如何把 state 转变成下一个 state
  4. * state 的形式取决于你,可以是基本类型、数组、对象、
  5. * 甚至是 Immutable.js 生成的数据结构。惟一的要点是
  6. * state 变化时需要返回全新的对象,而不是修改传入的参数。
  7. * 下面使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper)
  8. * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。*/
  9. function counter(state = 0, action) {
  10. switch (action.type) {
  11. case 'INCREMENT':
  12. return state + 1;
  13. case 'DECREMENT':
  14. return state - 1;
  15. default:
  16. return state;}}
  17. // 创建 Redux store 来存放应用的状态。
  18. // API { subscribe, dispatch, getState }。
  19. let store = createStore(counter);
  20. // 可以手动订阅更新,也可以事件绑定到视图层。
  21. store.subscribe(() => console.log(store.getState()));
  22. // 改变内部 state 惟一方法是 dispatch 一个 action
  23. // action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
  24. store.dispatch({ type: 'INCREMENT' });// 1
  25. store.dispatch({ type: 'INCREMENT' });// 2
  26. store.dispatch({ type: 'DECREMENT' });// 1

你应该把要做的修改变成一个普通对象,这个对象被叫做 action,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 reducer
如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。
它的美在于做复杂应用和庞大系统时优秀的扩展能力。由于它可以用 action 追溯应用的每一次修改,因此才有强大的开发工具。如录制用户会话并回放所有 action 来重现它。

要想更新 state 中的数据,需要发起一个 action。Action 就是一个普通 JavaScript 对象用来描述发生了什么。 action 的示例:

  1. { type: 'ADD_TODO', text: 'Go to swimming pool' }
  2. { type: 'TOGGLE_TODO', index: 1 }
  3. { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

强制使用 action 来描述所有变化带来的好处是可以清晰地知道应用中到底发生了什么。如果一些东西改变了,就可以知道为什么变。action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,开发一些函数,这就是 reducer。reducer 只是一个接收 state 和 action,并返回新的 state 的函数。

三原则

单一数据源。整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。

  1. console.log(store.getState())
  2. /* 输出
  3. {visibilityFilter: 'SHOW_ALL',
  4. todos: [
  5. {text: 'Consider using Redux',completed: true,},
  6. { text: 'Keep all state in a single tree',completed: false}]}*/

State是只读的。唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。

  1. store.dispatch({type: 'COMPLETE_TODO',index: 1})
  2. store.dispatch({type: 'SET_VISIBILITY_FILTER',filter: 'SHOW_COMPLETED'})

使用纯函数来执行修改。为了描述 action 如何改变 state tree ,你需要编写 reducers
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。

  1. function visibilityFilter(state = 'SHOW_ALL', action) {
  2. switch (action.type) {
  3. case 'SET_VISIBILITY_FILTER':
  4. return action.filter
  5. default:
  6. return state}}
  7. function todos(state = [], action) {
  8. switch (action.type) {
  9. case 'ADD_TODO':
  10. return [...state,
  11. {text: action.text,
  12. completed: false}]
  13. case 'COMPLETE_TODO':
  14. return state.map((todo, index) => {
  15. if (index === action.index) {
  16. return Object.assign({}, todo, {completed: true})}
  17. return todo})
  18. default:
  19. return state}}
  20. import { combineReducers, createStore } from 'redux'
  21. let reducer = combineReducers({ visibilityFilter, todos })
  22. let store = createStore(reducer)

action

Action 是把数据从应用(之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般会通过store.dispatch()通过将 action 传到 store。
添加新 todo 任务的 action 是这样的:

  1. const ADD_TODO = 'ADD_TODO'
  1. {type: ADD_TODO,
  2. text: 'Build my first Redux app'}

Action 本质上是 JavaScript 普通对象。action 内必须使用一个字符串类型的type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。

  1. import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

样板文件使用提醒
使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。参照 减少样板代码 获取更多保持代码简洁的实践经验
还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 index来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。

  1. { type: TOGGLE_TODO,
  2. index: 5}

我们应该尽量减少在 action 中传递的数据。比如上面的例子,传递 index 就比把整个任务对象传过去要好。
最后,再添加一个 action type 来表示当前的任务展示选项。

  1. {type: SET_VISIBILITY_FILTER,
  2. filter: SHOW_COMPLETED}

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
在 Redux 中的 action 创建函数只是简单的返回一个 action:

  1. function addTodo(text) {
  2. return {type: ADD_TODO,text}}

这样做将使 action 创建函数更容易被移植和测试。
传统的 Flux 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,举例:

  1. function addTodoWithDispatch(text) {
  2. const action = {type: ADD_TODO,text}
  3. dispatch(action)}

不同的是,Redux 中只需把 action 创建函数的结果传给 disptch()方法即可发起一次 dispatch 过程。

  1. dispatch(addTodo(text))
  2. dispatch(completeTodo(index))

或者创建一个 被绑定的 action 创建函数 来自动 dispatch:

  1. const boundAddTodo = text => dispatch(addTodo(text))
  2. const boundCompleteTodo = index => dispatch(completeTodo(index))

然后直接调用它们:

  1. boundAddTodo(text);
  2. boundCompleteTodo(index);

store 里能直接通过 store.dispatch()调用 dispatch()方法,但是多数情况下会使用 react-redux提供的 connect帮助器来调用。bindActionCreators()可以自动把多个 action 创建函数 绑定到 dispatch()方法上。
Action 创建函数也可以是异步非纯函数。 异步 action可以处理 AJAX 响应和如何把 action 创建函数组合进异步控制流。

源码:action。js

  1. /*action 类型*/
  2. export const ADD_TODO = 'ADD_TODO';
  3. export const TOGGLE_TODO = 'TOGGLE_TODO'
  4. export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
  5. /*其它的常量*/
  6. export const VisibilityFilters = {
  7. SHOW_ALL: 'SHOW_ALL',
  8. SHOW_COMPLETED: 'SHOW_COMPLETED',
  9. SHOW_ACTIVE: 'SHOW_ACTIVE'}
  10. /*action 创建函数*/
  11. export function addTodo(text) {
  12. return { type: ADD_TODO, text }}
  13. export function toggleTodo(index) {
  14. return { type: TOGGLE_TODO, index }}
  15. export function setVisibilityFilter(filter) {
  16. return { type: SET_VISIBILITY_FILTER, filter }}

reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
设计 state 结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?
以 todo 应用为例,需要保存两种不同的数据:

  • 当前选中的任务过滤条件;
  • 完整的任务列表。

通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。

  1. {visibilityFilter: 'SHOW_ALL',
  2. todos: [
  3. {text: 'Consider using Redux',
  4. completed: true,},
  5. { text: 'Keep all state in a single tree',
  6. completed: false}]}

处理 Reducer 关系时的注意事项
开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById:{ id -> todo }和 todos:array是比较好的方式。
Action 处理
现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

  1. (previousState, action) => newState

之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如Date.now()或Math.randow()。

需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 action
我们将以指定 state 的初始状态作为开始。Redux 首次执行时,state 为undefined ,此时我们可借机设置并返回应用的初始 state。

  1. import { VisibilityFilters } from './actions'
  2. const initialState = {visibilityFilter: VisibilityFilters.SHOW_ALL,todos: []};
  3. function todoApp(state, action) {
  4. if (typeof state === 'undefined') { return initialState}
  5. // 这里暂不处理任何 action,// 仅返回传入的 state
  6. return state}

这里一个技巧是使用 ES6 参数默认值语法 来精简代码。

  1. function todoApp(state = initialState, action) {
  2. // 这里暂不处理任何 action,// 仅返回传入的 state
  3. return state}

现在可以处理SET_VISIBILITY_FILTER。需要做的只是改变 state 中的visibilityFilter。

  1. function todoApp(state = initialState, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return Object.assign({}, state, {visibilityFilter: action.filter})
  5. default:
  6. return state}}

注意:

  1. 不要修改 state。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state,{ visibilityFilter:action.filter}) ,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用{…state,…newState} 达到相同的目的。
  2. 在 defalut情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。

Object.assign() 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它库如_.assign() 提供的帮助方法。

switch和样板代码须知

switch 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。
很不幸到现在为止,还有很多人存在一个误区:根据文档中是否使用 switch 来决定是否使用它。如果你不喜欢 switch,完全可以自定义一个 createReduce函数来接收一个事件处理函数列表,参照“减少样板代码”
处理多个action

还有两个 action 需要处理。就像我们处理SET_VISIBILITY_FILTER 一样,我们引入ADD_TODO和TOGGLE_TODO 两个actions 并且扩展我们的 reducer 去处理 ADD_TODO.

  1. import {ADD_TODO,
  2. TOGGLE_TODO,
  3. SET_VISIBILITY_FILTER,
  4. VisibilityFilters
  5. } from './actions'
  6. ...
  7. function todoApp(state = initialState, action) {
  8. switch (action.type) {
  9. case SET_VISIBILITY_FILTER:
  10. return Object.assign({}, state, {visibilityFilter: action.filter})
  11. case ADD_TODO:
  12. return Object.assign({}, state, {todos: [
  13. ...state.todos,{text: action.text,completed: false}]})
  14. default:
  15. return state}}

如上,不直接修改 state 中的字段,而是返回新对象。新的 todos对象就相当于旧的 todos在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。
最后,TOGGLE_TODO 的实现也很好理解:

  1. case TOGGLE_TODO:
  2. return Object.assign({}, state, {todos: state.todos.map((todo, index) => {
  3. if (index === action.index) {
  4. return Object.assign({}, todo, {completed: !todo.completed})}
  5. return todo})})

我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。(译者注:Javascript中的对象存储时均是由值和指向值的引用两个部分构成。此处突变指直接修改引用所指向的值, 而引用本身保持不变。) 如果经常需要这类的操作,可以选择使用帮助类 React-addons-updateupdeep,或者使用原生支持深度更新的库 Immutable。最后,时刻谨记永远不要在克隆 state前修改它。

拆分Reducer
目前的代码看起来有些冗长:

  1. function todoApp(state = initialState, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return Object.assign({}, state, {visibilityFilter: action.filter})
  5. case ADD_TODO:
  6. return Object.assign({}, state, {todos: [ ...state.todos,
  7. {text: action.text,completed: false}]})
  8. case TOGGLE_TODO:
  9. return Object.assign({}, state, {todos: state.todos.map((todo, index) => {
  10. if (index === action.index) {
  11. return Object.assign({}, todo, {completed: !todo.completed})}
  12. return todo})})
  13. default:
  14. return state}}

上面代码能否变得更通俗易懂?这里的 todos和visibilityFilter 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 todos更新的业务逻辑拆分到一个单独的函数里:

  1. function todos(state = [], action) {
  2. switch (action.type) {
  3. case ADD_TODO:
  4. return [...state,{text: action.text,completed: false}]
  5. case TOGGLE_TODO:
  6. return state.map((todo, index) => {if (index === action.index) {
  7. return Object.assign({}, todo, {completed: !todo.completed})}
  8. return todo})
  9. default:
  10. return state}}
  11. function todoApp(state = initialState, action) {
  12. switch (action.type) {
  13. case SET_VISIBILITY_FILTER:
  14. return Object.assign({}, state, {visibilityFilter: action.filter})
  15. case ADD_TODO:
  16. return Object.assign({}, state, {todos: todos(state.todos, action)})
  17. case TOGGLE_TODO:
  18. return Object.assign({}, state, {todos: todos(state.todos, action)})
  19. default:
  20. return state}}

注意 todos依旧接收 state,但它变成了一个数组!现在todoApp只把需要更新的一部分 state 传给 todos 函数, todos函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。
下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理visibilityFilter ?当然可以:
首先引用, 让我们使用 ES6 对象结构 去声明 SHOW_ALL:

  1. const { SHOW_ALL } = VisibilityFilters

接下来:

  1. function visibilityFilter(state = SHOW_ALL, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return action.filter
  5. default:
  6. return state}}

现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入undefined , 子 reducer 将负责返回它们的默认值。

  1. function todos(state = [], action) {
  2. switch (action.type) {
  3. case ADD_TODO:
  4. return [...state,{text: action.text,completed: false}]
  5. case TOGGLE_TODO:
  6. return state.map((todo, index) => {
  7. if (index === action.index) {
  8. return Object.assign({}, todo, {completed: !todo.completed})}
  9. return todo})
  10. default:
  11. return state}}
  12. function visibilityFilter(state = SHOW_ALL, action) {
  13. switch (action.type) {
  14. case SET_VISIBILITY_FILTER:
  15. return action.filter
  16. default:
  17. return state}}
  18. function todoApp(state = {}, action) {
  19. return {visibilityFilter: visibilityFilter(state.visibilityFilter, action),
  20. todos: todos(state.todos, action)}}

注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state参数都不同,分别对应它管理的那部分 state 数据。
现在看起来好多了!随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。
最后,Redux 提供了combineReducers() 工具类来做上面todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构todoApp :

  1. import { combineReducers } from 'redux'
  2. const todoApp = combineReducers({visibilityFilter,todos})
  3. export default todoApp

注意上面的写法和下面完全等价:

  1. export default function todoApp(state = {}, action) {
  2. return {visibilityFilter: visibilityFilter(state.visibilityFilter, action),
  3. todos: todos(state.todos, action)}}

你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:

  1. const reducer = combineReducers({
  2. a: doSomethingWithA,
  3. b: processB,
  4. c: c})
  1. function reducer(state = {}, action) {
  2. return {
  3. a: doSomethingWithA(state.a, action),
  4. b: processB(state.b, action),
  5. c: c(state.c, action)}}

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

使用注意
combineReducers接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过export 暴露出每个 reducer 函数,然后使用import * as reducers 得到一个以它们名字作为 key 的 object:

  1. import { combineReducers } from 'redux'
  2. import * as reducers from './reducers'
  3. const todoApp = combineReducers(reducers)

源码:reducers.js

  1. import { combineReducers } from 'redux'
  2. import {ADD_TODO,TOGGLE_TODO,SET_VISIBILITY_FILTER,VisibilityFilters} from './actions'
  3. const { SHOW_ALL } = VisibilityFilters
  4. function visibilityFilter(state = SHOW_ALL, action) {
  5. switch (action.type) {
  6. case SET_VISIBILITY_FILTER:
  7. return action.filter
  8. default:
  9. return state}}
  10. function todos(state = [], action) {
  11. switch (action.type) {
  12. case ADD_TODO:
  13. return [...state,{text: action.text,completed: false}]
  14. case TOGGLE_TODO:
  15. return state.map((todo, index) => {
  16. if (index === action.index) {
  17. return Object.assign({}, todo, {completed: !todo.completed})}
  18. return todo})
  19. default:
  20. return state}}
  21. const todoApp = combineReducers({visibilityFilter,todos})
  22. export default todoApp

Store

使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:

再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,应该使用 reducer 组合而不是创建多个 store。
根据已有的 reducer 来创建 store 是非常容易的。在面,我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()
import { createStore } from ‘redux’
import todoApp from ‘./reducers’
let store = createStore(todoApp)
createStore() 的第二个参数是可选的, 用于设置 state 初始状态。对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
发起 Actions
现在我们已经创建好了 store ,让我们来验证一下!虽然还没有界面,我们已经可以测试数据处理逻辑了。

  1. import {addTodo,toggleTodo,setVisibilityFilter,VisibilityFilters} from './actions'
  2. // 打印初始状态
  3. console.log(store.getState())
  4. // 每次 state 更新时,打印日志// 注意 subscribe() 返回一个函数用来注销监听器
  5. const unsubscribe = store.subscribe(() =>console.log(store.getState()))
  6. // 发起一系列 action
  7. store.dispatch(addTodo('Learn about actions'))
  8. store.dispatch(addTodo('Learn about reducers'))
  9. store.dispatch(addTodo('Learn about store'))
  10. store.dispatch(toggleTodo(0))
  11. store.dispatch(toggleTodo(1))
  12. store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
  13. // 停止监听 state 更新
  14. unsubscribe();

可以看到 store 里的 state 是如何变化的:image.png
可以看到,在还没有开发界面的时候,我们就可以定义程序的行为。而且这时候已经可以写 reducer 和 action 创建函数的测试。不需要模拟任何东西,因为它们都是纯函数。只需调用一下,对返回值做断言,写测试就是这么简单。
源码
index.js

  1. import { createStore } from 'redux'
  2. import todoApp from './reducers'
  3. let store = createStore(todoApp)

数据流

严格的单向数据流是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
动机Flux 案例, Redux 并不是严格意义上的 Flux,但它们有共同的设计思想。
Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)。 就是一个描述“发生了什么”的普通对象。比如:
  1. { type: 'LIKE_ARTICLE', articleId: 42 }
  2. { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
  3. { type: 'ADD_TODO', text: 'Read the Redux docs.' }

可以把 action 理解成新闻的摘要。如 “玛丽喜欢42号文章。” 或者 “任务列表里添加了’学习 Redux 文档’”。
可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。

  1. Redux store 调用传入的 reducer 函数。 会把两个参数传入 reducer: 当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据:
    1. // 当前应用的 statetodos 列表和选中的过滤器)
    2. let previousState = {
    3. visibleTodoFilter: 'SHOW_ALL',
    4. todos: [{text: 'Read the docs.',complete: false}]}
    5. // 将要执行的 action(添加一个 todo
    6. let action = {type: 'ADD_TODO',text: 'Understand the flow.'}
    7. // reducer 返回处理后的应用状态
    8. let nextState = todoApp(previousState, action)

注意 reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
3.根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
下面演示 combineReducers() 如何使用。假如你有两个 reducer:一个是 todo 列表,另一个是当前选择的过滤器设置:

  1. function todos(state = [], action) {// 省略处理逻辑...
  2. return nextState}
  3. function visibleTodoFilter(state = 'SHOW_ALL', action) {// 省略处理逻辑...
  4. return nextState}
  5. let todoApp = combineReducers({todos,visibleTodoFilter})

当你触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:
let nextTodos = todos(state.todos, action
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
然后会把两个结果集合并成一个 state 树:

  1. return
  2. todos: nextTodos,
  3. visibleTodoFilter: nextVisibleTodoFilter
  4. }

虽然 combineReducers() 是一个很方便的辅助工具,你也可以选择不用;你可以自行实现自己的根 reducer!
4.Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。
现在,可以应用新的 state 来更新 UI。如果你使用了 React Redux 这类的绑定库,这时就应该调用 component.setState(newState) 来更新。

异步Action

异步的应用。它将使用 Reddit API 来获取并显示指定 subreddit 下的帖子列表。那么 Redux 究竟是如何处理异步数据流的呢?
Action
当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(也可能是超时)。
这两个时刻都可能会更改应用的 state;为此,你需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都需要 dispatch 至少三种 action:

  • 一种通知 reducer 请求开始的 action。对于这种 action,reducer 可能会切换一下 state 中的 isFetching 标记。以此来告诉 UI 来显示加载界面。
  • 一种通知 reducer 请求成功的 action。对于这种 action,reducer 可能会把接收到的新数据合并到 state 中,并重置 isFetching。UI 则会隐藏加载界面,并显示接收到的数据。
  • 一种通知 reducer 请求失败的 action。对于这种 action,reducer 可能会重置 isFetching。另外,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

  1. { type: 'FETCH_POSTS' }
  2. { type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
  3. { type: 'FETCH_POSTS', status: 'success', response: { ... } }
  4. //又或者为它们定义不同的 type:
  5. { type: 'FETCH_POSTS_REQUEST' }
  6. { type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
  7. { type: 'FETCH_POSTS_SUCCESS', response: { ... } }

究竟使用带有标记位的同一个 action,还是多个 action type 呢,完全取决于你。这应该是你的团队共同达成的约定。使用多个 type 会降低犯错误的机率,但是如果你使用像 redux-actions 这类的辅助库来生成 action 创建函数和 reducer 的话,这就完全不是问题了。
无论使用哪种约定,一定要在整个应用中保持统一。在本教程中,我们将使用不同的 type 来做。
同步 Action 创建函数(Action Creator)
下面先定义几个同步的 action 类型 和 action 创建函数。比如,用户可以选择要显示的 subreddit:
actions.js

  1. export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
  2. export function selectSubreddit(subreddit) {
  3. return {type: SELECT_SUBREDDIT,subreddit}}

也可以按 “刷新” 按钮来更新它:

  1. export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
  2. export function invalidatesubreddit(subreddit) {
  3. return {type: INVALIDATE_SUBREDDIT,subreddit}}

这些是用户操作来控制的 action。也有另外一类 action,是由网络请求来控制。后面会介绍如何使用它们,现在,我们只是来定义它们。
当需要获取指定 subreddit 的帖子的时候,需要 dispatch REQUEST_POSTS action:

  1. export const REQUEST_POSTS = 'REQUEST_POSTS'
  2. export function requestPosts(subreddit) {
  3. return {type: REQUEST_POSTS, subreddit}}

把 REQUEST_POSTS 和 SELECT_SUBREDDIT 或 INVALIDATE_SUBREDDIT 分开很重要。虽然它们的发生有先后顺序,但随着应用变得复杂,有些用户操作(比如,预加载最流行的 subreddit,或者一段时间后自动刷新过期数据)后需要马上请求数据。路由变化时也可能需要请求数据,所以一开始如果把请求数据和特定的 UI 事件耦合到一起是不明智的。
最后,当收到请求响应时,我们会 dispatch RECEIVE_POSTS:

  1. export const RECEIVE_POSTS = 'RECEIVE_POSTS'
  2. export function receivePosts(subreddit, json) {
  3. return {type: RECEIVE_POSTS,subreddit,
  4. posts: json.data.children.map(child => child.data),receivedAt: Date.now()}}

以上就是现在需要知道的所有内容。稍后会介绍如何把 dispatch action 与网络请求结合起来。
错误处理须知
在实际应用中,网络请求失败时也需要 dispatch action。虽然在本教程中我们并不做错误处理,但是这个 真实场景的案例 会演示一种实现方案。
设计 state 结构
就像在基础教程中,在功能开发前你需要 设计应用的 state 结构。在写异步代码的时候,需要考虑更多的 state,所以我们要仔细考虑一下。
这部分内容通常让初学者感到迷惑,因为选择哪些信息才能清晰地描述异步应用的 state 并不直观,还有怎么用一个树来把这些信息组织起来。
我们以最通用的案例来打头:列表。Web 应用经常需要展示一些内容的列表。比如,帖子的列表,朋友的列表。首先要明确应用要显示哪些列表。然后把它们分开储存在 state 中,这样你才能对它们分别做缓存并且在需要的时候再次请求更新数据。
“Reddit 头条” 应用会长这个样子:

  1. {selectedsubreddit: 'frontend',
  2. postsBySubreddit: {
  3. frontend: {isFetching: true,didInvalidate: false, items: []},
  4. reactjs: {isFetching: false,didInvalidate: false,lastUpdated: 1439478405547,
  5. items: [{id: 42,title: 'Confusion about Flux and Relay'},
  6. {id: 500,
  7. title: 'Creating a Simple Application Using React JS and Flux Architecture'}]
  8. }}}

下面列出几个要点:

  • 分开存储 subreddit 信息,是为了缓存所有 subreddit。当用户来回切换 subreddit 时,可以立即更新,同时在不需要的时候可以不请求数据。不要担心把所有帖子放到内存中(会浪费内存):除非你需要处理成千上万条帖子,同时用户还很少关闭标签页,否则你不需要做任何清理。
  • 每个帖子的列表都需要使用 isFetching 来显示进度条,didInvalidate 来标记数据是否过期,lastUpdated 来存放数据最后更新时间,还有 items 存放列表信息本身。在实际应用中,你还需要存放 fetchedPageCount 和 nextPageUrl 这样分页相关的 state。

嵌套内容须知
在这个示例中,接收到的列表和分页信息是存在一起的。但是,这种做法并不适用于有互相引用的嵌套内容的场景,或者用户可以编辑列表的场景。想像一下用户需要编辑一个接收到的帖子,但这个帖子在 state tree 的多个位置重复出现。这会让开发变得非常困难。
如果你有嵌套内容,或者用户可以编辑接收到的内容,你需要把它们分开存放在 state 中,就像数据库中一样。在分页信息中,只使用它们的 ID 来引用。这可以让你始终保持数据更新。真实场景的案例 中演示了这种做法,结合 normalizr 来把嵌套的 API 响应数据范式化,最终的 state 看起来是这样:

  1. {selectedsubreddit: 'frontend',
  2. entities: {
  3. users: {
  4. 2: {id: 2,name: 'Andrew'}},
  5. posts: {
  6. 42: {id: 42,title: 'Confusion about Flux and Relay', author: 2},
  7. 100: {id: 100,
  8. title: 'Creating a Simple Application Using React JS and Flux Architecture',
  9. author: 2}}},
  10. postsBySubreddit: {
  11. frontend: {isFetching: true,didInvalidate: false,items: []},
  12. reactjs: {isFetching: false,didInvalidate: false,lastUpdated: 1439478405547,
  13. items: [ 42, 100 ]}}}

在本教程中,我们不会对内容进行范式化,但是在一个复杂些的应用中你可能需要使用。
处理 Action
在讲 dispatch action 与网络请求结合使用细节前,我们为上面定义的 action 开发一些 reducer。
Reducer 组合须知
这里,我们假设你已经学习过 combineReducers() 并理解 reducer 组合,还有 基础章节 中的 拆分 Reducer。如果还没有,请 先学习
reducers.js

  1. import { combineReducers } from 'redux'
  2. import {SELECT_SUBREDDIT,INVALIDATE_SUBREDDIT,REQUEST_POSTS,RECEIVE_POSTS
  3. } from '../actions'
  4. function selectedsubreddit(state = 'reactjs', action) {
  5. switch (action.type) {
  6. case SELECT_SUBREDDIT:
  7. return action.subreddit
  8. default:
  9. return state}}
  10. function posts(
  11. state = {isFetching: false,didInvalidate: false,items: []},action) {
  12. switch (action.type) {
  13. case INVALIDATE_SUBREDDIT:
  14. return Object.assign({}, state, {didInvalidate: true})
  15. case REQUEST_POSTS:
  16. return Object.assign({}, state, {isFetching: true, didInvalidate: false})
  17. case RECEIVE_POSTS:
  18. return Object.assign({}, state, {isFetching: false,didInvalidate: false,
  19. items: action.posts,lastUpdated: action.receivedAt})
  20. default:
  21. return state}}
  22. function postsBySubreddit(state = {}, action) {
  23. switch (action.type) {
  24. case INVALIDATE_SUBREDDIT:
  25. case RECEIVE_POSTS:
  26. case REQUEST_POSTS:
  27. return Object.assign({}, state, {
  28. [action.subreddit]: posts(state[action.subreddit], action)})
  29. default:
  30. return state}}
  31. const rootReducer = combineReducers({postsBySubreddit,selectedsubreddit})
  32. export default rootReducer

上面代码有两个有趣的点:

  • 使用 ES6 计算属性语法,使用 Object.assign() 来简洁高效地更新 state[action.subreddit]。这个:
    1. return Object.assign({}, state, {
    2. [action.subreddit]: posts(state[action.subreddit], action)})
  • 与下面代码等价:
    1. let nextState = {}
    2. nextState[action.subreddit] = posts(state[action.subreddit], action)
    3. return Object.assign({}, state, nextState)
  • 我们提取出 posts(state, action) 来管理指定帖子列表的 state。这就是 reducer 组合 !我们还可以借此机会把 reducer 分拆成更小的 reducer,这种情况下,我们把对象内列表的更新代理到了 posts reducer 上。在 真实场景的案例 中甚至更进一步,里面介绍了如何做一个 reducer 工厂来生成参数化的分页 reducer。

记住 reducer 只是函数而已,所以你可以尽情使用函数组合和高阶函数这些特性。
异步 action 创建函数
最后,如何把 之前定义 的同步 action 创建函数和网络请求结合起来呢?标准的做法是使用 Redux Thunk 中间件。要引入 redux-thunk 这个专门的库才能使用。我们 后面 会介绍 middleware 大体上是如何工作的;目前,你只需要知道一个要点:通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk
当 action 创建函数返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。
我们仍可以在 actions.js 里定义这些特殊的 thunk action 创建函数。
actions.js

  1. import fetch from 'cross-fetch'
  2. export const REQUEST_POSTS = 'REQUEST_POSTS'
  3. function requestPosts(subreddit) {
  4. return {type: REQUEST_POSTS,subreddit}}
  5. export const RECEIVE_POSTS = 'RECEIVE_POSTS'
  6. function receivePosts(subreddit, json) {
  7. return {type: RECEIVE_POSTS,subreddit,
  8. posts: json.data.children.map(child => child.data),receivedAt: Date.now()}}
  9. export const INVALIDATE_SUBREDDIT = INVALIDATE_SUBREDDIT
  10. export function invalidateSubreddit(subreddit) {
  11. return {type: INVALIDATE_SUBREDDIT,subreddit}}
  12. // 来看一下我们写的第一个 thunk action 创建函数!
  13. //虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
  14. // store.dispatch(fetchPosts('reactjs'))
  15. export function fetchPosts(subreddit) {
  16. // Thunk middleware 知道如何处理函数。这里把 dispatch 方法通过参数的形式传给函数,
  17. // 以此来让它自己也能 dispatch action
  18. return function (dispatch) {
  19. // 首次 dispatch:更新应用的 state 来通知// API 请求发起了。
  20. dispatch(requestPosts(subreddit))
  21. // thunk middleware 调用的函数可以有返回值,// 它会被当作 dispatch 方法的返回值传递。
  22. // 这个案例中,我们返回一个等待处理的 promise
  23. // 这并不是 redux middleware 所必须的,但这对于我们而言很方便。
  24. return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
  25. .then(response => response.json(),
  26. // 不要使用 catch,因为会捕获// dispatch 和渲染中出现的任何错误,
  27. // 导致 'Unexpected batch number' 错误。
  28. // https://github.com/facebook/react/issues/6895
  29. error => console.log('An error occurred.', error))
  30. .then(json =>
  31. // 可以多次 dispatch
  32. // 这里,使用 API 请求结果来更新应用的 state
  33. dispatch(receivePosts(subreddit, json)))}}

fetch 使用须知
本示例使用了 fetch API。它是替代 XMLHttpRequest 用来发送网络请求的非常新的 API。由于目前大多数浏览器原生还不支持它,建议你使用 cross_fetch 库:
// 每次使用 fetch 前都这样调用一下
import fetch from ‘cross_fetch’
在底层,它在浏览器端使用 whatwg-fetch polyfill,在服务器端使用 node-fetch,所以如果当你把应用改成 同构 时,并不需要改变 API 请求。
注意,fetch polyfill 假设你已经使用了 Promise 的 polyfill。确保你使用 Promise polyfill 的一个最简单的办法是在所有应用代码前启用 Babel 的 ES6 polyfill:
// 在应用中其它任何代码执行前调用一次
import ‘babel-polyfill’
我们是如何在 dispatch 机制中引入 Redux Thunk middleware 的呢?我们使用了 applyMiddleware(),如下:
index.js

  1. import thunkMiddleware from 'redux-thunk'
  2. import { createLogger } from 'redux-logger'
  3. import { createStore, applyMiddleware } from 'redux'
  4. import { selectSubreddit, fetchPosts } from './actions'
  5. import rootReducer from './reducers'
  6. const loggerMiddleware = createLogger()
  7. const store = createStore(
  8. rootReducer,applyMiddleware(thunkMiddleware, // 允许我们 dispatch() 函数
  9. loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志))
  10. store.dispatch(selectSubreddit('reactjs'))
  11. store
  12. .dispatch(fetchPosts('reactjs'))
  13. .then(() => console.log(store.getState()))
  14. //thunk 的一个优点是它的结果可以再次被 dispatch

actions.js

  1. import fetch from 'cross-fetch'
  2. export const REQUEST_POSTS = 'REQUEST_POSTS'
  3. function requestPosts(subreddit) {
  4. return {type: REQUEST_POSTS,subreddit}}
  5. export const RECEIVE_POSTS = 'RECEIVE_POSTS'
  6. function receivePosts(subreddit, json) {
  7. return {type: RECEIVE_POSTS,subreddit,
  8. posts: json.data.children.map(child => child.data),receivedAt: Date.now()}}
  9. export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
  10. export function invalidateSubreddit(subreddit) {
  11. return {type: INVALIDATE_SUBREDDIT,subreddit}}
  12. function fetchPosts(subreddit) {
  13. return dispatch => {dispatch(requestPosts(subreddit))
  14. return fetch(`http://www.reddit.com/r/${subreddit}.json`)
  15. .then(response => response.json())
  16. .then(json => dispatch(receivePosts(subreddit, json)))}}
  17. function shouldFetchPosts(state, subreddit) {
  18. const posts = state.postsBySubreddit[subreddit]
  19. if (!posts) {return true}
  20. else if (posts.isFetching) {return false}
  21. else {return posts.didInvalidate}}
  22. export function fetchPostsIfNeeded(subreddit) {// 注意这个函数也接收了 getState() 方法
  23. // 它让你选择接下来 dispatch 什么。// 当缓存的值是可用时,// 减少网络请求很有用。
  24. return (dispatch, getState) => {
  25. if (shouldFetchPosts(getState(), subreddit)) {// thunk dispatch 另一个 thunk
  26. return dispatch(fetchPosts(subreddit))}
  27. else {// 告诉调用代码不需要再等待。
  28. return Promise.resolve()}}}//这可以让我们逐步开发复杂的异步控制流,同时保持代码整洁如初:

index.js

  1. store
  2. .dispatch(fetchPostsIfNeeded('reactjs'))
  3. .then(() => console.log(store.getState()))

服务端渲染须知
异步 action 创建函数对于做服务端渲染非常方便。你可以创建一个 store,dispatch 一个异步 action 创建函数,这个 action 创建函数又 dispatch 另一个异步 action 创建函数来为应用的一整块请求数据,同时在 Promise 完成和结束时才 render 界面。然后在 render 前,store 里就已经存在了需要用的 state。
Thunk middleware 并不是 Redux 处理异步 action 的唯一方式:

你也可以先尝试一些不同做法,选择喜欢的,并使用下去,不论有没有使用到 middleware 都行。
连接到 UI
Dispatch 同步 action 与异步 action 间并没有区别。

异步数据流

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流
你可以使用 applyMiddleware() 来增强 createStore()。虽然这不是必须的,但是它可以帮助你用简便的方式来描述异步的 action
redux-thunkredux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。比如,支持 Promise 的 middleware 能够拦截 Promise,然后为每个 Promise 异步地 dispatch 一对 begin/end actions。
当 middleware 链中的最后一个 middleware 开始 dispatch action 时,这个 action 必须是一个普通对象。这是 同步式的 Redux 数据流 开始的地方(译注:这里应该是指,你可以使用任意多异步的 middleware 去做你想做的事情,但是需要使用普通对象作为最后一个被 dispatch 的 action ,来将处理流程带回同步方式)。
接着可以查看 异步示例的完整源码

middleware 中间键

我们已经在异步 Action 一节的示例中看到了一些 middleware 的使用。如果你使用过 Express 或者 Koa 等服务端框架, 那么应该对 middleware 的概念不会陌生。 在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers、记录日志、内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。
相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是类似的。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
这个章节分为两个部分,前面是帮助你理解相关概念的深度介绍,而后半部分则通过一些实例来体现 middleware 的强大能力。对文章前后内容进行结合通读,会帮助你更好的理解枯燥的概念,并从中获得启发。
理解middleware
正因为 middleware 可以完成包括异步 API 调用在内的各种事情,了解它的演化过程是一件相当重要的事。我们将以记录日志和创建崩溃报告为例,引导你体会从分析问题到通过构建 middleware 解决问题的思维过程。
问题:记录日志
使用 Redux 的一个益处就是它让 state 的变化过程变的可预知和透明。每当一个 action 发起完成后,新的 state 就会被计算并保存下来。State 不能被自身修改,只能由特定的 action 引起变化。
试想一下,当我们的应用中每一个 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。
Redux架构 - 图2
我们如何通过 Redux 实现它呢?
尝试#1:手动记录
最直接的解决方案就是在每次调用store.dispatch(action) 前后手动记录被发起的 action 和新的 state。这称不上一个真正的解决方案,仅仅是我们理解这个问题的第一步。
注意
如果你使用 react-redux 或者类似的绑定库,最好不要直接在你的组件中操作 store 的实例。在接下来的内容中,仅仅是假设你会通过 store 显式地向下传递。
假设,你在创建一个 Todo 时这样调用:

  1. store.dispatch(addTodo('Use Redux'))

为了记录这个 action 以及产生的新的 state,你可以通过这种方式记录日志:

  1. let action = addTodo('Use Redux')
  2. console.log('dispatching', action)
  3. store.dispatch(action)
  4. console.log('next state', store.getState())

虽然这样做达到了想要的效果,但是你并不想每次都这么干。
尝试#2:封装 Dispatch
你可以将上面的操作抽取成一个函数:

  1. function dispatchAndLog(store, action) {
  2. console.log('dispatching', action)
  3. store.dispatch(action)
  4. console.log('next state', store.getState())
  5. }

然后用它替换store.dispatch():

  1. dispatchAndLog(store, addTodo('Use Redux'))

你可以选择到此为止,但是每次都要导入一个外部方法总归还是不太方便。
尝试#3:Monkeypatching Dispatch
如果我们直接替换 store 实例中的 dispatch 函数会怎么样呢?Redux store 只是一个包含一些方法的普通对象,同时我们使用的是 JavaScript,因此我们可以这样实现dispatch的 monkeypatch:

  1. let next = store.dispatch
  2. store.dispatch = function dispatchAndLog(action) {
  3. console.log('dispatching', action)
  4. let result = next(action)
  5. console.log('next state', store.getState())
  6. return result}

这离我们想要的已经非常接近了!无论我们在哪里发起 action,保证都会被记录。Monkeypatching 令人感觉还是不太舒服,不过利用它我们做到了我们想要的。
问题:崩溃报告
如果我们想对 dispatch 附加超过一个的变换,又会怎么样呢?
我脑海中出现的另一个常用的变换就是在生产过程中报告 JavaScript 的错误。全局的window.onerror 并不可靠,因为它在一些旧的浏览器中无法提供错误堆栈,而这是排查错误所需的至关重要信息。
试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到类似于 Sentry 这样的报告服务中,不是很好吗?这样我们可以更容易地在开发环境中重现这个错误。
然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。(提示:我们正在慢慢了解 middleware 的本质到底是什么!)
如果按照我们的想法,日志记录和崩溃报告属于不同的模块,他们看起来应该像这样:

  1. function patchStoreToAddLogging(store) {
  2. let next = store.dispatch
  3. store.dispatch = function dispatchAndLog(action) {
  4. console.log('dispatching', action)
  5. let result = next(action)
  6. console.log('next state', store.getState())
  7. return result}}
  8. function patchStoreToAddCrashReporting(store) {
  9. let next = store.dispatch
  10. store.dispatch = function dispatchAndReportErrors(action) {
  11. try {
  12. return next(action)
  13. } catch (err) {
  14. console.error('捕获一个异常!', err)
  15. Raven.captureException(err, {
  16. extra: {
  17. action,
  18. state: store.getState()}})
  19. throw err}}}

如果这些功能以不同的模块发布,我们可以在 store 中像这样使用它们:

  1. patchStoreToAddLogging(store)
  2. patchStoreToAddCrashReporting(store)

尽管如此,这种方式看起来还是不是够令人满意。
尝试#4:隐藏 Monkeypatching
Monkeypatching 本质上是一种 hack。“将任意的方法替换成你想要的”,此时的 API 会是什么样的呢?现在,让我们来看看这种替换的本质。 在之前,我们用自己的函数替换掉了store.dispatch 。如果我们不这样做,而是在函数中返回新的 dispatch呢?

  1. function logger(store) {
  2. let next = store.dispatch
  3. // 我们之前的做法:
  4. // store.dispatch = function dispatchAndLog(action) {
  5. return function dispatchAndLog(action) {
  6. console.log('dispatching', action)
  7. let result = next(action)
  8. console.log('next state', store.getState())
  9. return result}}

我们可以在 Redux 内部提供一个可以将实际的 monkeypatching 应用到 store.dispatch 中的辅助方法:

  1. function applyMiddlewareByMonkeypatching(store, middlewares) {
  2. middlewares = middlewares.slice()
  3. middlewares.reverse()
  4. // 在每一个 middleware 中变换 dispatch 方法。
  5. middlewares.forEach(middleware =>
  6. store.dispatch = middleware(store))}

然后像这样应用多个 middleware:

  1. applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])

尽管我们做了很多,实现方式依旧是 monkeypatching。
因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。
尝试#5: 移除 Monkeypatching
为什么我们要替换原来的 dispatch 呢?当然,这样我们就可以在后面直接调用它,但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 store.dispatch:

  1. function logger(store) {
  2. // 这里的 next 必须指向前一个 middleware 返回的函数:
  3. let next = store.dispatch
  4. return function dispatchAndLog(action) {
  5. console.log('dispatching', action)
  6. let result = next(action)
  7. console.log('next state', store.getState())
  8. return result}}

将 middleware 串连起来的必要性是显而易见的。
如果applyMiddlewareByMonkeypatching 方法中没有在第一个 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的dispatch 方法。
但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个next() 方法,而不是通过 store 的实例去获取。

  1. function logger(store) {
  2. return function wrapDispatchToAddLogging(next) {
  3. return function dispatchAndLog(action) {
  4. console.log('dispatching', action)
  5. let result = next(action)
  6. console.log('next state', store.getState())
  7. return result}}}

现在是“我们该更进一步”的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些串联函数很吓人。ES6 的箭头函数可以使其 柯里化 ,从而看起来更舒服一些:

  1. const logger = store => next => action => {
  2. console.log('dispatching', action)
  3. let result = next(action)
  4. console.log('next state', store.getState())
  5. return result
  6. }
  7. const crashReporter = store => next => action => {
  8. try {
  9. return next(action)
  10. } catch (err) {
  11. console.error('Caught an exception!', err)
  12. Raven.captureException(err, {
  13. extra: {
  14. action,
  15. state: store.getState()}})
  16. throw err}}

这正是 Redux middleware 的样子。
Middleware 接收了一个next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的next(),以此类推。由于 store 中类似getState() 的方法依旧非常有用,我们将 store作为顶层的参数,使得它可以在所有 middleware 中被使用。
尝试#6: ‘单纯’使用 Middleware
我们可以写一个applyMiddleware() 方法替换掉原来的applyMiddlewareByMonkeypatching() 。在新的 applyMiddleware() 中,我们取得最终完整的被包装过的 dispatch() 函数,并返回一个 store 的副本:

  1. // 警告:这只是一种“单纯”的实现方式!
  2. // *并不是* Redux API.
  3. function applyMiddleware(store, middlewares) {
  4. middlewares = middlewares.slice()
  5. middlewares.reverse()
  6. let dispatch = store.dispatch
  7. middlewares.forEach(middleware =>
  8. dispatch = middleware(store)(dispatch))
  9. return Object.assign({}, store, { dispatch })}

这与 Redux 中applyMiddleware()的实现已经很接近了,但是有三个重要的不同之处

  • 它只暴露一个 store API 的子集给 middleware:dispatch(action)和 getState()。
  • 它用了一个非常巧妙的方式,以确保如果你在 middleware 中调用的是store.dispatch(action) 而不是next(action) ,那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。这对异步的 middleware 非常有用,正如我们在之前的章节中提到的。
  • 为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 Store本身。因此它的签名不是(store,middleware)=> store,而是(…middlewares)=>(createStore)=>createStore 。

由于在使用之前需要先应用方法到createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。
最终的方法
这是我们刚刚所写的 middleware:

  1. const logger = store => next => action => {
  2. console.log('dispatching', action)
  3. let result = next(action)
  4. console.log('next state', store.getState())
  5. return result}
  6. const crashReporter = store => next => action => {
  7. try {
  8. return next(action)
  9. } catch (err) {
  10. console.error('Caught an exception!', err)
  11. Raven.captureException(err, {
  12. extra: {
  13. action,
  14. state: store.getState()}})
  15. throw err}}

然后是将它们引用到 Redux store 中:

  1. import { createStore, combineReducers, applyMiddleware } from 'redux'
  2. let todoApp = combineReducers(reducers)
  3. let store = createStore(
  4. todoApp,
  5. // applyMiddleware() 告诉 createStore() 如何处理中间件
  6. applyMiddleware(logger, crashReporter))

就是这样!现在任何被发送到 store 的 action 都会经过logger和 crashReporter:

  1. // 将经过 logger crashReporter 两个 middleware
  2. store.dispatch(addTodo('Use Redux'))

七个实例
如果读完上面的章节你已经觉得头都要爆了,那就想象一下把它写出来之后的样子。下面的内容会让我们放松一下,并让你的思路延续。
下面的每个函数都是一个有效的 Redux middleware。它们不是同样有用,但是至少他们一样有趣。

  1. /**
  2. * 记录所有被发起的 action 以及产生的新的 state
  3. */
  4. const logger = store => next => action => {
  5. console.group(action.type)
  6. console.info('dispatching', action)
  7. let result = next(action)
  8. console.log('next state', store.getState())
  9. console.groupEnd(action.type)
  10. return result}
  11. /**在 state 更新完成和 listener 被通知之后发送崩溃报告。*/
  12. const crashReporter = store => next => action => {
  13. try {
  14. return next(action)
  15. } catch (err) {
  16. console.error('Caught an exception!', err)
  17. Raven.captureException(err, {
  18. extra: {
  19. action,
  20. state: store.getState()}})
  21. throw err}}
  22. /**用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。
  23. 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。*/
  24. const timeoutScheduler = store => next => action => {
  25. if (!action.meta || !action.meta.delay) {
  26. return next(action)}
  27. let timeoutId = setTimeout(
  28. () => next(action),
  29. action.meta.delay)
  30. return function cancel() {
  31. clearTimeout(timeoutId)}}
  32. /**通过 { meta: { raf: true } } action 在一个 rAF 循环帧中被发起。
  33. * 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。*/
  34. const rafScheduler = store => next => {
  35. let queuedActions = []
  36. let frame = null
  37. function loop() {
  38. frame = null
  39. try {
  40. if (queuedActions.length) {
  41. next(queuedActions.shift())}
  42. } finally {
  43. maybeRaf()}}
  44. function maybeRaf() {
  45. if (queuedActions.length && !frame) {
  46. frame = requestAnimationFrame(loop)}}
  47. return action => {
  48. if (!action.meta || !action.meta.raf) {
  49. return next(action)}
  50. queuedActions.push(action)
  51. maybeRaf()
  52. return function cancel() {
  53. queuedActions = queuedActions.filter(a => a !== action)}}}
  54. /**使你除了 action 之外还可以发起 promise
  55. * 如果这个 promise resolved,他的结果将被作为 action 发起。
  56. * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。*/
  57. const vanillaPromise = store => next => action => {
  58. if (typeof action.then !== 'function') {
  59. return next(action)}
  60. return Promise.resolve(action).then(store.dispatch)}
  61. /**让你可以发起带有一个 { promise } 属性的特殊 action
  62. * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve
  63. 发起另一个成功(或失败)的 action。为了方便,`dispatch` 会返回这个 promise 让调用者可以等待。*/
  64. const readyStatePromise = store => next => action => {
  65. if (!action.promise) {
  66. return next(action)}
  67. function makeAction(ready, data) {
  68. let newAction = Object.assign({}, action, { ready }, data)
  69. delete newAction.promise
  70. return newAction}
  71. next(makeAction(false))
  72. return action.promise.then(
  73. result => next(makeAction(true, { result })),
  74. error => next(makeAction(true, { error })))}
  75. /**让你可以发起一个函数来替代 action。这个函数接收 `dispatch` `getState` 作为参数。
  76. 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,
  77. 这非常有用。`dispatch` 会返回被发起函数的返回值。 */
  78. const thunk = store => next => action =>
  79. typeof action === 'function' ?
  80. action(store.dispatch, store.getState) :
  81. next(action)
  82. // 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。)
  83. let todoApp = combineReducers(reducers)
  84. let store = createStore(
  85. todoApp,
  86. applyMiddleware(
  87. rafScheduler,
  88. timeoutScheduler,
  89. thunk,
  90. vanillaPromise,
  91. readyStatePromise,
  92. logger,
  93. crashReporter))