手写 redux 源码 - 图1

  1. |-src
  2. |-utils
  3. |-actionType.js //
  4. |-formatProdErrorMessage.js // 不写
  5. |-isPlainObject.js // 判断是否为纯对象
  6. |-kindOf.js // 用来判断类型
  7. |-symbol-observable.js // 不写
  8. |-warning.js // 负责控制台错误日志的输出,不写
  9. |-applyMiddleware.js // 我们使用它应用中间件时,它返回一个 enhancer 函数作为 createStore 的第三个参数
  10. |-bindActionCreator.js // 用得不多。把 action creators 转成拥有同名 keys 的对象,使用 dispatch 把每个 action creator 包装起来,这样可以直接调用它们。
  11. |-combineReducers.js // 将 reducers 组合起来
  12. |-compose.js // 函数组合,工具函数来的
  13. |-createStore.js // 创建 store
  14. |-index.js // 入口文件

github 仓库:https://github.com/Konsoue/redux-learn

手写 redux

  1. 我们从入口 index.js 开始写起。
  2. 我们使用 redux 的时候肯定是要创建 store,那么就来看看 createStore 做了什么事情
  3. 在讲函数式编程的时候,我们有了解过函数组合 compose,这里的 compose.js 就是这样
  4. 在使用 redux 写多个 reducer 的时候,我们会最后用 combindReducer 把这些 reducer 合在一起
  5. 应用中间件的时候,我们会使用 applyMiddleware,所以看看这个函数做了什么操作

index.js

  1. import createStore from './createStore';
  2. import compose from './compose';
  3. import combineReducers from './combineReducers';
  4. import applyMiddleware from './applyMiddleware';
  5. import bindActionCreators from './bindActionCreators';
  6. export {
  7. createStore,
  8. compose,
  9. combineReducers,
  10. applyMiddleware,
  11. bindActionCreators
  12. }

createStore.js

  1. import isPlainObject from "./utils/isPlainObject"
  2. import kindOf from "./utils/kindOf"
  3. /**
  4. *
  5. * @param {Function} reducer
  6. * @param {*} preloadedState 初始的 state
  7. * @param {Function} enhancer 增强器,applyMiddleware 的返回值。
  8. * @returns {Store}
  9. */
  10. function createStore(reducer, preloadedState, enhancer) {
  11. // 如果有中间件,将 createStore 交给中间件创建,并传入 reducer, preloadedState
  12. if (kindOf(enhancer) !== 'undefined') {
  13. return enhancer(createStore)(reducer, preloadedState);
  14. }
  15. let currentReducer = reducer
  16. let currentState = preloadedState
  17. let currentListeners = [] // 当前的 listeners ,即所有监听函数
  18. let nextListeners = currentListeners // 在 state 数据改变前,订阅的监听函数,先放在 nextListeners,以保证 currrentListeners 数据修改前的不变性。
  19. let isDispatching = false // reducers 正在执行的标志
  20. /**
  21. * 派发 action,这是改变 state 的唯一途径
  22. * @param {Object} action 一个纯对象,代表了将要改变什么。
  23. * @returns {Object} 为了方便,返回用户派发的 action
  24. */
  25. const dispatch = (action) => {
  26. if (!isPlainObject(action)) {
  27. throw new Error(`Actions 必须是纯对象。而当前 Actions 的类型是${kindOf(action)}。你可以加中间件, 来处理特殊类型`)
  28. }
  29. if (!action.type) throw new Error('Actions 必须有一个 type 属性')
  30. if (isDispatching) throw new Error('还不可以给 reducers 派发 action')
  31. try {
  32. // Reducers 开始处理 action
  33. isDispatching = true
  34. currentState = currentReducer(currentState, action)
  35. } finally {
  36. isDispatching = false
  37. }
  38. // 处理完 action, 触发监听事件
  39. // 先把 nextListeners 赋值给 currentListeners
  40. // 因为 subscribe 添加监听函数时,会将监听函数先放到 nextListeners
  41. // 以此来保证 currentListeners 触发前,自身的不变性
  42. const listeners = (currentListeners = nextListeners)
  43. for (let i = 0; i < listeners.length; i++) {
  44. const listener = listeners[i]
  45. listener()
  46. }
  47. return action
  48. }
  49. /**
  50. * 将 currentListeners 浅拷贝后,赋给 nextListeners
  51. */
  52. const ensureCanMutateNextListeners = () => {
  53. if (nextListeners === currentListeners) {
  54. nextListeners = currentListeners.slice()
  55. }
  56. }
  57. /**
  58. * 订阅:添加一个监听函数
  59. * @param {Function} listener 监听函数
  60. * @return {Function} 返回一个函数用于移除,这个监听函数
  61. */
  62. const subscribe = (listener) => {
  63. if (isDispatching) throw new Error('reducer 正在执行, 你不能调用 store.subscribe()')
  64. let isSubscribed = true // 标志该 listener 是否已经订阅
  65. ensureCanMutateNextListeners()
  66. nextListeners.push(listener)
  67. // 返回一个取消订阅的函数
  68. return function unsubscribe() {
  69. if (!isSubscribed) return
  70. if (isDispatching) throw new Error('reducer 正在执行, 你不能调用 unsubscribe()')
  71. isSubscribed = false
  72. ensureCanMutateNextListeners()
  73. const index = nextListeners.indexOf(listener)
  74. nextListeners.splice(index, 1)
  75. currentListeners = null // 这里赋值为 null,释放内存。不会影响数据改变后,调用监听函数的
  76. }
  77. }
  78. /**
  79. * 读取被 store 管理的 state 树
  80. * @returns {any} 在你的项目中,目前的 state 树
  81. */
  82. const getState = () => {
  83. if (isDispatching) throw new Error('reducer 正在执行, 你不能调用 store.getState()')
  84. return currentState
  85. }
  86. /**
  87. * 替换 reducer
  88. * @param {Function} nextReducer
  89. */
  90. const replaceReducer = (nextReducer) => {
  91. currentReducer = nextReducer
  92. }
  93. return {
  94. dispatch,
  95. subscribe,
  96. getState,
  97. replaceReducer,
  98. }
  99. }
  100. export default createStore

compose.js

  1. const compose = (...fns) => {
  2. if (fns.length === 0) return (args) => args
  3. if (fns.length === 1) return fns[0]
  4. fns.reduce((pre, cur) => (...args) => pre(cur(...args)))
  5. }
  6. export default compose

combindReducer.js

  1. import kindOf from "./utils/kindOf"
  2. /**
  3. * 将外部传进来的 reducers 合成一个总的 reducers
  4. * @param {Object} reducers
  5. * @returns {Function}
  6. */
  7. const combineReducers = (reducers) => {
  8. const reducerKeys = Object.keys(reducers)
  9. const resultReducer = (state = {}, action) => {
  10. const nextState = {}
  11. for (let i = 0; i < reducerKeys.length; i++) {
  12. const key = reducerKeys[i]
  13. const reducer = reducers[key]
  14. const previousStateForKey = state[key] // 拿到某个 reducer 处理前对应的 state
  15. const nextStateForKey = reducer(previousStateForKey, action) // 拿到处理后的 state
  16. if (kindOf(nextStateForKey) === 'undefined') {
  17. throw new Error(`${key} reducer 处理结果不能是 undefined`)
  18. }
  19. nextState[key] = nextStateForKey
  20. }
  21. return nextState
  22. }
  23. return resultReducer
  24. }
  25. export default combineReducers

从源码 reducers[key]state[key]可以看出,我们使用 redux 创建 reducer 和 state 的时候,key 需要一一对应。

  1. import { combindReducer, createStore } from 'redux'
  2. const initalState = {
  3. home: {},
  4. login: {},
  5. user: {}
  6. }
  7. const homeReducer = (state, action) => {}
  8. const userReducer = (state, action) => {}
  9. const loginReducer = (state, action) => {}
  10. // reducers 的 key 要和 initalState 对应
  11. const reducers = {
  12. home: homeReducer,
  13. user: userReducer,
  14. login: loginReducer
  15. }
  16. const resultReducer = combineReducers(reducers)
  17. const store = createStore(reducers, initalState)

applyMiddleware.js

  1. import compose from './compose'
  2. /**
  3. * 我们使用它应用中间件,使得中间件可以调用 redux 的 store 的方法
  4. *
  5. * @param {...Function} middlewares
  6. * @returns {Function} 返回的函数,用于 createStore 的第三个参数
  7. */
  8. const applyMiddleware = (...middlewares) => {
  9. const enhancer = (createStore) => {
  10. const newCreateStore = (reducers, preloadState) => {
  11. const store = createStore(reducers, preloadState)
  12. // chain = [ (next) => {}, (next) => {}, ...]
  13. // 其中的 next 方法是我们下面将要传入的 store.dispatch
  14. const chain = middlewares.map(middleware => middleware(store))
  15. // 一系列中间件对 dispatch 的包裹,形成一个新的 dispatch。
  16. // 所以外界调用 store.dispatch 就是调用 newDispatch
  17. const newDispatch = compose(...chain)(store.dispatch)
  18. return {
  19. ...store,
  20. dispatch: newDispatch
  21. }
  22. }
  23. return newCreateStore
  24. }
  25. return enhancer
  26. }
  27. export default applyMiddleware

utils

isPlainObject.js

判断对象是否为纯对象(简单对象)。
纯对象:通过{}new Object()创建的都是纯对象。

有的认为Object.create(null)创建的对象也是纯对象。

  1. /**
  2. * @param {any} obj
  3. * @returns {boolean} ture 表示 obj 是纯对象
  4. */
  5. const isPlainObject = (obj) => {
  6. if (typeof obj !== 'object' || obj === null) return false;
  7. let proto = obj;
  8. while (Object.getPrototypeOf(proto) !== null) {
  9. proto = Object.getPrototypeOf(proto);
  10. }
  11. return Object.getPrototypeOf(obj) === proto
  12. }
  13. export default isPlainObject

为什么 redux 不直接判断 Object.prototype === Object.getPrototypeOf(obj)呢?
答:因为 redux 考虑了页面嵌入子页面的情况,而不同页面之间的内置对象都是独立创建出来的。
比如:Parent 页面,嵌入 Child 页面。Parent 的 Object 构造函数 !== Child 的 Object 构造函数
如果直接使用题目的判断方式,那么如果在 Child 页面 let obj = new Object()
在 Parent 页面的 redux 判断时,obj 的原型是 Child 的 Object 的原型,不等于 Parent 的 Object 的原型。
手写 redux 源码 - 图2

kindOf.js

redux 生产模式使用 typeof,开发模式自行封装了 miniKindOf 判断函数。
我们直接使用生产模式就好了。

  1. const kindOf = (val) => typeof val
  2. export default kindOf;

开发模式的方法,如下,有兴趣就看。

  1. // Inlined / shortened version of `kindOf` from https://github.com/jonschlinkert/kind-of
  2. function miniKindOf(val) {
  3. if (val === void 0) return 'undefined'
  4. if (val === null) return 'null'
  5. const type = typeof val
  6. switch (type) {
  7. case 'boolean':
  8. case 'string':
  9. case 'number':
  10. case 'symbol':
  11. case 'function': {
  12. return type
  13. }
  14. default:
  15. break
  16. }
  17. if (Array.isArray(val)) return 'array'
  18. if (isDate(val)) return 'date'
  19. if (isError(val)) return 'error'
  20. const constructorName = ctorName(val)
  21. switch (constructorName) {
  22. case 'Symbol':
  23. case 'Promise':
  24. case 'WeakMap':
  25. case 'WeakSet':
  26. case 'Map':
  27. case 'Set':
  28. return constructorName
  29. default:
  30. break
  31. }
  32. // other
  33. return type.slice(8, -1).toLowerCase().replace(/\s/g, '')
  34. }
  35. function ctorName(val) {
  36. return typeof val.constructor === 'function' ? val.constructor.name : null
  37. }
  38. function isError(val) {
  39. return (
  40. val instanceof Error ||
  41. (typeof val.message === 'string' &&
  42. val.constructor &&
  43. typeof val.constructor.stackTraceLimit === 'number')
  44. )
  45. }
  46. function isDate(val) {
  47. if (val instanceof Date) return true
  48. return (
  49. typeof val.toDateString === 'function' &&
  50. typeof val.getDate === 'function' &&
  51. typeof val.setDate === 'function'
  52. )
  53. }

测试用例

  1. import { createStore, combineReducers, applyMiddleware } from '../src'
  2. import { logMiddleware, reduxThunk } from './middleware'
  3. const initalState = {
  4. home: {},
  5. user: {
  6. name: 'jack'
  7. },
  8. count: 0
  9. }
  10. const homeReducer = (state, action) => {
  11. return state
  12. }
  13. const userReducer = (state, action) => {
  14. switch (action.type) {
  15. case 'changeName':
  16. return {
  17. ...state,
  18. name: action.data
  19. }
  20. default:
  21. return state
  22. }
  23. }
  24. const countReducer = (state, action) => {
  25. switch (action.type) {
  26. case 'increment':
  27. return state + 1
  28. case 'decrement':
  29. return state - 1
  30. default:
  31. return state
  32. }
  33. }
  34. // reducers 的 key 要和 initalState 对应
  35. const reducers = {
  36. home: homeReducer,
  37. user: userReducer,
  38. count: countReducer
  39. }
  40. // 将 reducer 组合起来
  41. const resultReducer = combineReducers(reducers)
  42. // 应用中间件
  43. const enhancer = applyMiddleware(logMiddleware, reduxThunk);
  44. const store = createStore(resultReducer, initalState, enhancer);
  45. // 订阅 store,当 store 的数据有任何变化,就会触发
  46. store.subscribe(() => console.log(store.getState()))
  47. store.dispatch({ type: 'increment' })
  48. store.dispatch({ type: 'increment' })
  49. const asyncAction = (dispatch, state) => {
  50. // 异步事件执行后,再 dispatch
  51. setTimeout(() => {
  52. dispatch({ type: 'changeName', data: 'konsoue' })
  53. }, 2000)
  54. }
  55. store.dispatch(asyncAction);
  1. const logMiddleware = (store) => (next) => (action) => {
  2. console.log('logMiddleware');
  3. return next(action);
  4. }
  5. const reduxThunk = (store) => (next) => (action) => {
  6. if (typeof action === 'function') {
  7. return action(store.dispatch, store.getState);
  8. }
  9. return next(action);
  10. }
  11. export {
  12. logMiddleware,
  13. reduxThunk
  14. }

参考资料

《github 的 redux 源码》
《手写一个Redux,深入理解其原理》