Redux 进阶教程

原文(保持更新):https://github.com/kenberkeley/redux-simple-tutorial/blob/master/redux-advanced-tutorial.md

写在前面

相信您已经看过 Redux 简明教程,本教程是简明教程的实战化版本,伴随源码分析
Redux 用的是 ES6 编写,看到有疑惑的地方的,可以复制粘贴到这里在线编译 ES5

§ Redux API 总览

在 Redux 的源码目录 src/,我们可以看到如下文件结构:

  1. ├── utils/
  2. ├── warning.js # 打酱油的,负责在控制台显示警告信息
  3. ├── applyMiddleware.js
  4. ├── bindActionCreators.js
  5. ├── combineReducers.js
  6. ├── compose.js
  7. ├── createStore.js
  8. ├── index.js # 入口文件

除去打酱油的 utils/warning.js 以及入口文件 index.js,剩下那 5 个就是 Redux 的 API

§ compose(…functions)

先说这个 API 的原因是它没有依赖,是一个纯函数

⊙ 源码分析

  1. /**
  2. * 看起来逼格很高,实际运用其实是这样子的:
  3. * compose(f, g, h)(...args) => f(g(h(...args)))
  4. *
  5. * 值得注意的是,它用到了 reduceRight,因此执行顺序是从右到左
  6. *
  7. * @param {多个函数,用逗号隔开}
  8. * @return {函数}
  9. */
  10. export default function compose(...funcs) {
  11. if (funcs.length === 0) {
  12. return arg => arg
  13. }
  14. if (funcs.length === 1) {
  15. return funcs[0]
  16. }
  17. const last = funcs[funcs.length - 1]
  18. const rest = funcs.slice(0, -1)
  19. return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  20. }

这里的关键点在于,reduceRight 可传入初始值:

  1. // 由于 reduce / reduceRight 仅仅是方向的不同,因此下面用 reduce 说明即可
  2. var arr = [1, 2, 3, 4, 5]
  3. var re1 = arr.reduce(function(total, i) {
  4. return total + i
  5. })
  6. console.log(re1) // 15
  7. var re2 = arr.reduce(function(total, i) {
  8. return total + i
  9. }, 100) // <---------------传入一个初始值
  10. console.log(re2) // 115

下面是 compose 的实例(在线演示):

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
  5. </head>
  6. <body>
  7. <script>
  8. function func1(num) {
  9. console.log('func1 获得参数 ' + num);
  10. return num + 1;
  11. }
  12. function func2(num) {
  13. console.log('func2 获得参数 ' + num);
  14. return num + 2;
  15. }
  16. function func3(num) {
  17. console.log('func3 获得参数 ' + num);
  18. return num + 3;
  19. }
  20. // 有点难看(如果函数名再长一点,那屏幕就不够宽了)
  21. var re1 = func3(func2(func1(0)));
  22. console.log('re1:' + re1);
  23. console.log('===============');
  24. // 很优雅
  25. var re2 = Redux.compose(func3, func2, func1)(0);
  26. console.log('re2:' + re2);
  27. </script>
  28. </body>
  29. </html>

控制台输出:

  1. func1 获得参数 0
  2. func2 获得参数 1
  3. func3 获得参数 3
  4. re16
  5. ===============
  6. func1 获得参数 0
  7. func2 获得参数 1
  8. func3 获得参数 3
  9. re26

§ createStore(reducer, initialState, enhancer)

⊙ 源码分析

  1. import isPlainObject from 'lodash/isPlainObject'
  2. import $$observable from 'symbol-observable'
  3. /**
  4. * 这是 Redux 的私有 action 常量
  5. * 长得太丑了,你不要鸟就行了
  6. */
  7. export var ActionTypes = {
  8. INIT: '@@redux/INIT'
  9. }
  10. /**
  11. * @param {函数} reducer 不多解释了
  12. * @param {对象} preloadedState 主要用于前后端同构时的数据同步
  13. * @param {函数} enhancer 很牛逼,可以实现中间件、时间旅行,持久化等
  14. * ※ Redux 仅提供 applyMiddleware 这个 Store Enhancer ※
  15. * @return {Store}
  16. */
  17. export default function createStore(reducer, preloadedState, enhancer) {
  18. // 这里省略的代码,到本文的最后再讲述(用于压轴你懂的)
  19. var currentReducer = reducer
  20. var currentState = preloadedState // 这就是整个应用的 state
  21. var currentListeners = [] // 用于存储订阅的回调函数,dispatch 后逐个执行
  22. var nextListeners = currentListeners //【悬念1:为什么需要两个 存放回调函数 的变量?】
  23. var isDispatching = false
  24. /**
  25. * 【悬念1·解疑】
  26. * 试想,dispatch 后,回调函数正在乖乖地被逐个执行(for 循环进行时)
  27. * 假设回调函数队列原本是这样的 [a, b, c, d]
  28. *
  29. * 现在 for 循环执行到第 3 步,亦即 a、b 已经被执行,准备执行 c
  30. * 但在这电光火石的瞬间,a 被取消订阅!!!
  31. *
  32. * 那么此时回调函数队列就变成了 [b, c, d]
  33. * 那么第 3 步就对应换成了 d!!!
  34. * c 被跳过了!!!这就是躺枪。。。
  35. *
  36. * 作为一个回调函数,最大的耻辱就是得不到执行
  37. * 因此为了避免这个问题,本函数会在上述场景中把
  38. * currentListeners 复制给 nextListeners
  39. *
  40. * 这样的话,dispatch 后,在逐个执行回调函数的过程中
  41. * 如果有新增订阅或取消订阅,都在 nextListeners 中操作
  42. * 让 currentListeners 中的回调函数得以完整地执行
  43. *
  44. * 既然新增是在 nextListeners 中 push,因此毫无疑问
  45. * 新的回调函数不会在本次 currentListeners 的循环体中被触发
  46. *
  47. * (上述事件发生的几率虽然很低,但还是严谨点比较好)
  48. */
  49. function ensureCanMutateNextListeners() { // <-------这货就叫做【ensure 哥】吧
  50. if (nextListeners === currentListeners) {
  51. nextListeners = currentListeners.slice()
  52. }
  53. }
  54. /**
  55. * 返回 state
  56. */
  57. function getState() {
  58. return currentState
  59. }
  60. /**
  61. * 负责注册回调函数的老司机
  62. *
  63. * 这里需要注意的就是,回调函数中如果需要获取 state
  64. * 那每次获取都请使用 getState(),而不是开头用一个变量缓存住它
  65. * 因为回调函数执行期间,有可能有连续几个 dispatch 让 state 改得物是人非
  66. * 而且别忘了,dispatch 之后,整个 state 是被完全替换掉的
  67. * 你缓存的 state 指向的可能已经是老掉牙的 state 了!!!
  68. *
  69. * @param {函数} 想要订阅的回调函数
  70. * @return {函数} 取消订阅的函数
  71. */
  72. function subscribe(listener) {
  73. if (typeof listener !== 'function') {
  74. throw new Error('Expected listener to be a function.')
  75. }
  76. var isSubscribed = true
  77. ensureCanMutateNextListeners() // 调用 ensure 哥保平安
  78. nextListeners.push(listener) // 新增订阅在 nextListeners 中操作
  79. // 返回一个取消订阅的函数
  80. return function unsubscribe() {
  81. if (!isSubscribed) {
  82. return
  83. }
  84. isSubscribed = false
  85. ensureCanMutateNextListeners() // 调用 ensure 哥保平安
  86. var index = nextListeners.indexOf(listener)
  87. nextListeners.splice(index, 1) // 取消订阅还是在 nextListeners 中操作
  88. }
  89. }
  90. /**
  91. * 改变应用状态 state 的不二法门:dispatch 一个 action
  92. * 内部的实现是:往 reducer 中传入 currentState 以及 action
  93. * 用其返回值替换 currentState,最后逐个触发回调函数
  94. *
  95. * 如果 dispatch 的不是一个对象类型的 action(同步的),而是 Promise / thunk(异步的)
  96. * 则需引入 redux-thunk 等中间件来反转控制权【悬念2:什么是反转控制权?】
  97. *
  98. * @param & @return {对象} action
  99. */
  100. function dispatch(action) {
  101. if (!isPlainObject(action)) {
  102. throw new Error(
  103. 'Actions must be plain objects. ' +
  104. 'Use custom middleware for async actions.'
  105. )
  106. }
  107. if (typeof action.type === 'undefined') {
  108. throw new Error(
  109. 'Actions may not have an undefined "type" property. ' +
  110. 'Have you misspelled a constant?'
  111. )
  112. }
  113. if (isDispatching) {
  114. throw new Error('Reducers may not dispatch actions.')
  115. }
  116. try {
  117. isDispatching = true
  118. // 关键点:currentState 与 action 会流通到所有的 reducer
  119. // 所有 reducer 的返回值整合后,替换掉当前的 currentState
  120. currentState = currentReducer(currentState, action)
  121. } finally {
  122. isDispatching = false
  123. }
  124. // 令 currentListeners 等于 nextListeners,表示正在逐个执行回调函数(这就是上面 ensure 哥的判定条件)
  125. var listeners = currentListeners = nextListeners
  126. // 逐个触发回调函数
  127. for (var i = 0; i < listeners.length; i++) {
  128. listeners[i]()
  129. /* 现在逐个触发回调函数变成了:
  130. var listener = listeners[i]
  131. listener() // 该中间变量避免了 this 指向 listeners 而造成泄露的问题 */
  132. // 在此衷心感谢 @BuptStEve 在 issue7 中指出之前我对此处的错误解读
  133. }
  134. return action // 为了方便链式调用,dispatch 执行完毕后,返回 action(下文会提到的,稍微记住就好了)
  135. }
  136. /**
  137. * 替换当前 reducer 的老司机
  138. * 主要用于代码分离按需加载、热替换等情况
  139. *
  140. * @param {函数} nextReducer
  141. */
  142. function replaceReducer(nextReducer) {
  143. if (typeof nextReducer !== 'function') {
  144. throw new Error('Expected the nextReducer to be a function.')
  145. }
  146. currentReducer = nextReducer // 就是这么简单粗暴!
  147. dispatch({ type: ActionTypes.INIT }) // 触发生成新的 state 树
  148. }
  149. /**
  150. * 这是留给 可观察/响应式库 的接口(详情 https://github.com/zenparsing/es-observable)
  151. * 如果您了解 RxJS 等响应式编程库,那可能会用到这个接口,否则请略过
  152. * @return {observable}
  153. */
  154. function observable() {略}
  155. // 这里 dispatch 只是为了生成 应用初始状态
  156. dispatch({ type: ActionTypes.INIT })
  157. return {
  158. dispatch,
  159. subscribe,
  160. getState,
  161. replaceReducer,
  162. [$$observable]: observable
  163. }
  164. }

【悬念2:什么是反转控制权? · 解疑】
在同步场景下,dispatch(action) 的这个 action 中的数据是同步获取的,并没有控制权的切换问题
但异步场景下,则需要将 dispatch 传入到回调函数。待异步操作完成后,回调函数自行调用 dispatch(action)

说白了:在异步 Action Creator 中自行调用 dispatch 就相当于反转控制权
您完全可以自己实现,也可以借助 redux-thunk / redux-promise 等中间件统一实现
(它们的作用也仅仅就是把 dispatch 等传入异步 Action Creator 罢了)

拓展阅读:阮老师的 Thunk 函数的含义与用法
题外话:您不觉得 JavaScript 的回调函数,就是反转控制权最普遍的体现吗?

§ combineReducers(reducers)

⊙ 应用场景

简明教程中的 code-7 如下:

  1. /** 本代码块记为 code-7 **/
  2. var initState = {
  3. counter: 0,
  4. todos: []
  5. }
  6. function reducer(state, action) {
  7. if (!state) state = initState
  8. switch (action.type) {
  9. case 'ADD_TODO':
  10. var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
  11. nextState.todos.push(action.payload)
  12. return nextState
  13. default:
  14. return state
  15. }
  16. }

上面的 reducer 仅仅是实现了 “新增待办事项” 的 state 的处理
我们还有计数器的功能,下面我们继续增加计数器 “增加 1” 的功能:

  1. /** 本代码块记为 code-8 **/
  2. var initState = { counter: 0, todos: [] }
  3. function reducer(state, action) {
  4. if (!state) return initState // 若是初始化可立即返回应用初始状态
  5. var nextState = _.cloneDeep(state) // 否则二话不说先克隆
  6. switch (action.type) {
  7. case 'ADD_TODO': // 新增待办事项
  8. nextState.todos.push(action.payload)
  9. break
  10. case 'INCREMENT': // 计数器加 1
  11. nextState.counter = nextState.counter + 1
  12. break
  13. }
  14. return nextState
  15. }

如果说还有其他的动作,都需要在 code-8 这个 reducer 中继续堆砌处理逻辑
但我们知道,计数器 与 待办事项 属于两个不同的模块,不应该都堆在一起写
如果之后又要引入新的模块(例如留言板),该 reducer 会越来越臃肿
此时就是 combineReducers 大显身手的时刻:

  1. 目录结构如下
  2. reducers/
  3. ├── index.js
  4. ├── counterReducer.js
  5. ├── todosReducer.js
  1. /** 本代码块记为 code-9 **/
  2. /* reducers/index.js */
  3. import { combineReducers } from 'redux'
  4. import counterReducer from './counterReducer'
  5. import todosReducer from './todosReducer'
  6. const rootReducer = combineReducers({
  7. counter: counterReducer, // 键名就是该 reducer 对应管理的 state
  8. todos: todosReducer
  9. })
  10. export default rootReducer
  11. -------------------------------------------------
  12. /* reducers/counterReducer.js */
  13. export default function counterReducer(counter = 0, action) { // 传入的 state 其实是 state.counter
  14. switch (action.type) {
  15. case 'INCREMENT':
  16. return counter + 1 // counter 是值传递,因此可以直接返回一个值
  17. default:
  18. return counter
  19. }
  20. }
  21. -------------------------------------------------
  22. /* reducers/todosReducers */
  23. export default function todosReducer(todos = [], action) { // 传入的 state 其实是 state.todos
  24. switch (action.type) {
  25. case 'ADD_TODO':
  26. return [ ...todos, action.payload ]
  27. default:
  28. return todos
  29. }
  30. }

code-8 reducercode-9 rootReducer 的功能是一样的,但后者的各个子 reducer 仅维护对应的那部分 state
其可操作性、可维护性、可扩展性大大增强

Flux 中是根据不同的功能拆分出多个 store 分而治之
而 Redux 只允许应用中有唯一的 store,通过拆分出多个 reducer 分别管理对应的 state


下面继续来深入使用 combineReducers。一直以来我们的应用状态都是只有两层,如下所示:

  1. state
  2. ├── counter: 0
  3. ├── todos: []

如果说现在又有一个需求:在待办事项模块中,存储用户每次操作(增删改)的时间,那么此时应用初始状态树应为:

  1. state
  2. ├── counter: 0
  3. ├── todo
  4. ├── optTime: []
  5. ├── todoList: [] # 这其实就是原来的 todos!

那么对应的 reducer 就是:

  1. 目录结构如下
  2. reducers/
  3. ├── index.js <-------------- combineReducers (生成 rootReducer)
  4. ├── counterReducer.js
  5. ├── todoReducers/
  6. ├── index.js <------ combineReducers
  7. ├── optTimeReducer.js
  8. ├── todoListReducer.js
  1. /* reducers/index.js */
  2. import { combineReducers } from 'redux'
  3. import counterReducer from './counterReducer'
  4. import todoReducers from './todoReducers/'
  5. const rootReducer = combineReducers({
  6. counter: counterReducer,
  7. todo: todoReducers
  8. })
  9. export default rootReducer
  10. =================================================
  11. /* reducers/todoReducers/index.js */
  12. import { combineReducers } from 'redux'
  13. import optTimeReducer from './optTimeReducer'
  14. import todoListReducer from './todoListReducer'
  15. const todoReducers = combineReducers({
  16. optTime: optTimeReducer,
  17. todoList: todoListReducer
  18. })
  19. export default todoReducers
  20. -------------------------------------------------
  21. /* reducers/todosReducers/optTimeReducer.js */
  22. export default function optTimeReducer(optTime = [], action) {
  23. // 咦?这里怎么没有 switch-case 分支?谁说 reducer 就一定包含 switch-case 分支的?
  24. return action.type.includes('TODO') ? [ ...optTime, new Date() ] : optTime
  25. }
  26. -------------------------------------------------
  27. /* reducers/todosReducers/todoListReducer.js */
  28. export default function todoListReducer(todoList = [], action) {
  29. switch (action.type) {
  30. case 'ADD_TODO':
  31. return [ ...todoList, action.payload ]
  32. default:
  33. return todoList
  34. }
  35. }

无论您的应用状态树有多么的复杂,都可以通过逐层下分管理对应部分的 state

  1. counterReducer(counter, action) -------------------- counter
  2. rootReducer(state, action) —→∑ optTimeReducer(optTime, action) ------ optTime nextState
  3. ↘—→∑ todo
  4. todoListReducer(todoList,action) ----- todoList
  5. 注:左侧表示 dispatch 分发流,∑ 表示 combineReducers;右侧表示各实体 reducer 的返回值,最后汇总整合成 nextState

看了上图,您应该能直观感受到为何取名为 reducer 了吧?把 state 分而治之,极大减轻开发与维护的难度

无论是 dispatch 哪个 action,都会流通所有的 reducer
表面上看来,这样子很浪费性能,但 JavaScript 对于这种纯函数的调用是很高效率的,因此请尽管放心
这也是为何 reducer 必须返回其对应的 state 的原因。否则整合状态树时,该 reducer 对应的键值就是 undefined

⊙ 源码分析

仅截取关键部分,毕竟有很大一部分都是类型检测警告

  1. function combineReducers(reducers) {
  2. var reducerKeys = Object.keys(reducers)
  3. var finalReducers = {}
  4. for (var i = 0; i < reducerKeys.length; i++) {
  5. var key = reducerKeys[i]
  6. if (typeof reducers[key] === 'function') {
  7. finalReducers[key] = reducers[key]
  8. }
  9. }
  10. var finalReducerKeys = Object.keys(finalReducers)
  11. // 返回合成后的 reducer
  12. return function combination(state = {}, action) {
  13. var hasChanged = false
  14. var nextState = {}
  15. for (var i = 0; i < finalReducerKeys.length; i++) {
  16. var key = finalReducerKeys[i]
  17. var reducer = finalReducers[key]
  18. var previousStateForKey = state[key] // 获取当前子 state
  19. var nextStateForKey = reducer(previousStateForKey, action) // 执行各子 reducer 中获取子 nextState
  20. nextState[key] = nextStateForKey // 将子 nextState 挂载到对应的键名
  21. hasChanged = hasChanged || nextStateForKey !== previousStateForKey
  22. }
  23. return hasChanged ? nextState : state
  24. }
  25. }

在此我的注释很少,因为代码写得实在是太过明了了,注释反而影响阅读
作者 Dan 用了大量的 for 循环,的确有点不够优雅

§ bindActionCreators(actionCreators, dispatch)

这个 API 有点鸡肋,它无非就是做了这件事情:dispatch(ActionCreator(XXX))

⊙ 源码分析

  1. /* 为 Action Creator 加装上自动 dispatch 技能 */
  2. function bindActionCreator(actionCreator, dispatch) {
  3. return (...args) => dispatch(actionCreator(...args))
  4. }
  5. export default function bindActionCreators(actionCreators, dispatch) {
  6. // 省去一大坨类型判断
  7. var keys = Object.keys(actionCreators)
  8. var boundActionCreators = {}
  9. for (var i = 0; i < keys.length; i++) {
  10. var key = keys[i]
  11. var actionCreator = actionCreators[key]
  12. if (typeof actionCreator === 'function') {
  13. // 逐个装上自动 dispatch 技能
  14. boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
  15. }
  16. }
  17. return boundActionCreators
  18. }

⊙ 应用场景

简明教程中的 code-5 如下:

  1. <--! 本代码块记为 code-5 -->
  2. <input id="todoInput" type="text" />
  3. <button id="btn">提交</button>
  4. <script>
  5. $('#btn').on('click', function() {
  6. var content = $('#todoInput').val() // 获取输入框的值
  7. var action = addTodo(content) // 执行 Action Creator 获得 action
  8. store.dispatch(action) // 手动显式 dispatch 一个 action
  9. })
  10. </script>

我们看到,调用 addTodo 这个 Action Creator 后得到一个 action,之后又要手动 dispatch(action)
如果是只有一个两个 Action Creator 还是可以接受,但如果有很多个那就显得有点重复了(其实我觉得不重复哈哈哈)
这个时候我们就可以利用 bindActionCreators 实现自动 dispatch

  1. <input id="todoInput" type="text" />
  2. <button id="btn">提交</button>
  3. <script>
  4. // 全局引入 Redux、jQuery,同时 store 是全局变量
  5. var actionsCreators = Redux.bindActionCreators(
  6. { addTodo: addTodo },
  7. store.dispatch // 传入 dispatch 函数
  8. )
  9. $('#btn').on('click', function() {
  10. var content = $('#todoInput').val()
  11. actionCreators.addTodo(content) // 它会自动 dispatch
  12. })
  13. </script>

综上,这个 API 没啥卵用,尤其是异步场景下,基本用不上

§ applyMiddleware(…middlewares)

Redux 中文文档 高级 · Middleware 有提到中间件的演化由来

首先要理解何谓 Middleware,何谓 Enhancer

⊙ Middleware

说白了,Redux 引入中间件机制,其实就是为了在 dispatch 前后,统一“做爱做的事”。。。
诸如统一的日志记录、引入 thunk 统一处理异步 Action Creator 等都属于中间件
下面是一个简单的打印动作前后 state 的中间件:

  1. /* 装逼写法 */
  2. const printStateMiddleware = ({ getState }) => next => action => {
  3. console.log('state before dispatch', getState())
  4. let returnValue = next(action)
  5. console.log('state after dispatch', getState())
  6. return returnValue
  7. }
  8. -------------------------------------------------
  9. /* 降低逼格写法 */
  10. function printStateMiddleware(middlewareAPI) { // 记为【锚点-1】,中间件内可用的 API
  11. return function (dispatch) { // 记为【锚点-2】,传入上级中间件处理逻辑(若无则为原 store.dispatch)
  12. // 下面记为【锚点-3】,整个函数将会被传到下级中间件(如果有的话)作为它的 dispatch 参数
  13. return function (action) { // <---------------------------------------------- 这货就叫做【中间件处理逻辑哥】吧
  14. console.log('state before dispatch', middlewareAPI.getState())
  15. var returnValue = dispatch(action) // 还记得吗,dispatch 的返回值其实还是 action
  16. console.log('state after dispatch', middlewareAPI.getState())
  17. return returnValue // 将 action 返回给上一个中间件(实际上可以返回任意值,或不返回)
  18. // 在此衷心感谢 @zaleGZL 在 issue15 中指出之前我对此处的错误解读
  19. }
  20. }
  21. }

⊙ Store Enhancer

说白了,Store 增强器就是对生成的 store API 进行改造,这是它与中间件最大的区别(中间件不修改 store 的 API)
而改造 store 的 API 就要从它的缔造者 createStore 入手。例如,Redux 的 API applyMiddleware 就是一个 Store 增强器:

  1. import compose from './compose' // 这货的作用其实就是 compose(f, g, h)(action) => f(g(h(action)))
  2. /* 传入一坨中间件 */
  3. export default function applyMiddleware(...middlewares) {
  4. /* 传入 createStore */
  5. return function(createStore) {
  6. /* 返回一个函数签名跟 createStore 一模一样的函数,亦即返回的是一个增强版的 createStore */
  7. return function(reducer, preloadedState, enhancer) {
  8. // 用原 createStore 先生成一个 store,其包含 getState / dispatch / subscribe / replaceReducer 四个 API
  9. var store = createStore(reducer, preloadedState, enhancer)
  10. var dispatch = store.dispatch // 指向原 dispatch
  11. var chain = [] // 存储中间件的数组
  12. // 提供给中间件的 API(其实都是 store 的 API)
  13. var middlewareAPI = {
  14. getState: store.getState,
  15. dispatch: (action) => dispatch(action)
  16. }
  17. // 给中间件“装上” API,见上面 ⊙Middleware【降低逼格写法】的【锚点-1】
  18. chain = middlewares.map(middleware => middleware(middlewareAPI))
  19. // 串联所有中间件
  20. dispatch = compose(...chain)(store.dispatch)
  21. // 例如,chain 为 [M3, M2, M1],而 compose 是从右到左进行“包裹”的
  22. // 那么,M1 的 dispatch 参数为 store.dispatch(见【降低逼格写法】的【锚点-2】)
  23. // 往后,M2 的 dispatch 参数为 M1 的中间件处理逻辑哥(见【降低逼格写法】的【锚点-3】)
  24. // 同样,M3 的 dispatch 参数为 M2 的中间件处理逻辑哥
  25. // 最后,我们得到串联后的中间件链:M3(M2(M1(store.dispatch)))
  26. //(这种形式的串联类似于洋葱,可参考文末的拓展阅读:中间件的洋葱模型)
  27. // 在此衷心感谢 @ibufu 在 issue8 中指出之前我对此处的错误解读
  28. return {
  29. ...store, // store 的 API 中保留 getState / subsribe / replaceReducer
  30. dispatch // 新 dispatch 覆盖原 dispatch,往后调用 dispatch 就会触发 chain 内的中间件链式串联执行
  31. }
  32. }
  33. }
  34. }

最终返回的虽然还是 store 的那四个 API,但其中的 dispatch 函数的功能被增强了,这就是所谓的 Store Enhancer

⊙ 综合应用 ( 在线演示 )

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
  5. </head>
  6. <body>
  7. <script>
  8. /** Action Creators */
  9. function inc() {
  10. return { type: 'INCREMENT' };
  11. }
  12. function dec() {
  13. return { type: 'DECREMENT' };
  14. }
  15. function reducer(state, action) {
  16. state = state || { counter: 0 };
  17. switch (action.type) {
  18. case 'INCREMENT':
  19. return { counter: state.counter + 1 };
  20. case 'DECREMENT':
  21. return { counter: state.counter - 1 };
  22. default:
  23. return state;
  24. }
  25. }
  26. function printStateMiddleware(middlewareAPI) {
  27. return function (dispatch) {
  28. return function (action) {
  29. console.log('dispatch 前:', middlewareAPI.getState());
  30. var returnValue = dispatch(action);
  31. console.log('dispatch 后:', middlewareAPI.getState(), '\n');
  32. return returnValue;
  33. };
  34. };
  35. }
  36. var enhancedCreateStore = Redux.applyMiddleware(printStateMiddleware)(Redux.createStore);
  37. var store = enhancedCreateStore(reducer);
  38. store.dispatch(inc());
  39. store.dispatch(inc());
  40. store.dispatch(dec());
  41. </script>
  42. </body>
  43. </html>

控制台输出:

  1. dispatch 前:{ counter: 0 }
  2. dispatch 后:{ counter: 1 }
  3. dispatch 前:{ counter: 1 }
  4. dispatch 后:{ counter: 2 }
  5. dispatch 前:{ counter: 2 }
  6. dispatch 后:{ counter: 1 }

实际上,上面生成 store 的代码可以更加优雅:

  1. /** 本代码块记为 code-10 **/
  2. var store = Redux.createStore(
  3. reducer,
  4. Redux.applyMiddleware(printStateMiddleware)
  5. )

如果有多个中间件以及多个增强器,还可以这样写(请留意序号顺序):

重温一下 createStore 完整的函数签名:function createStore(reducer, preloadedState, enhancer)

  1. /** 本代码块记为 code-11 **/
  2. import { createStore, applyMiddleware, compose } from 'redux'
  3. const store = createStore(
  4. reducer,
  5. preloadedState, // 可选,前后端同构的数据同步
  6. compose( // 还记得吗?compose 是从右到左的哦!
  7. applyMiddleware( // 这货也是 Store Enhancer 哦!但这是关乎中间件的增强器,必须置于 compose 执行链的最后
  8. middleware1,
  9. middleware2,
  10. middleware3
  11. ),
  12. enhancer3,
  13. enhancer2,
  14. enhancer1
  15. )
  16. )

为什么会支持那么多种写法呢?在 createStore 的源码分析的开头部分,我省略了一些代码,现在奉上该压轴部分:

  1. /** 本代码块记为 code-12 **/
  2. if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
  3. // 这里就是上面 code-10 的情况,只传入 reducer 和 Store Enhancer 这两个参数
  4. enhancer = preloadedState
  5. preloadedState = undefined
  6. }
  7. if (typeof enhancer !== 'undefined') {
  8. if (typeof enhancer !== 'function') {
  9. throw new Error('Expected the enhancer to be a function.')
  10. }
  11. // 存在 enhancer 就立即执行,返回增强版的 createStore <--------- 记为【锚点 12-1】
  12. return enhancer(createStore)(reducer, preloadedState)
  13. }
  14. if (typeof reducer !== 'function') {
  15. throw new Error('Expected the reducer to be a function.')
  16. }
  17. // 除 compose 外,createStore 竟然也在此为我们提供了书写的便利与自由度,实在是太体贴了

如果像 code-11 那样有多个 enhancer,则 code-12 【锚点 12-1】 中的代码会执行多次
生成最终的超级增强版 store。最后,奉上 code-11compose 内部的执行顺序示意图:

  1. createStore ————
  2. return enhancer1(createStore)(reducer, preloadedState, enhancer2)
  3. |
  4. ├———————→ createStore 增强版 1
  5. return enhancer2(createStore1)(reducer, preloadedState, enhancer3)
  6. |
  7. ├———————————→ createStore 增强版 1+2
  8. return enhancer3(createStore1+2)(reducer, preloadedState, applyMiddleware(m1,m2,m3))
  9. |
  10. ├————————————————————→ createStore 增强版 1+2+3
  11. return appleMiddleware(m1,m2,m3)(createStore1+2+3)(reducer, preloadedState)
  12. |
  13. ├——————————————————————————————————→ 生成最终增强版 store

§ 总结

Redux 有五个 API,分别是:

  • createStore(reducer, [initialState])
  • combineReducers(reducers)
  • applyMiddleware(...middlewares)
  • bindActionCreators(actionCreators, dispatch)
  • compose(...functions)

createStore 生成的 store 有四个 API,分别是:

  • getState()
  • dispatch(action)
  • subscribe(listener)
  • replaceReducer(nextReducer)

至此,若您已经理解上述 API 的作用机理,以及中间件与增强器的概念/区别
本人将不胜荣幸,不妨点个 star 算是对我的赞赏
如您对本教程有任何意见或改进的建议,欢迎 issue,我会尽快予您答复

最后奉上 React + Redux + React Router 的简易留言板实例:react-demo

拓展阅读:中间件的洋葱模型

tip