内容简介:已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下;捡起旧知识的同时又有了一些新的收获,在这里作文以记之。在阅读文章之前,最好已经知道如何使用 Redux(不是 React-Redux)。为了更好的解读源码,我们可以把源码拷贝到本地,然后搭建一个开发环境。Redux 的使用不依赖于 React,所以你完全可以在一个极为简单的 JavaScript 项目中使用它。这里不再赘述开发环境的搭建过程,需要的同学可以直接拷贝我的代码到本地,然后安装依赖,运行项
已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下;捡起旧知识的同时又有了一些新的收获,在这里作文以记之。
在阅读文章之前,最好已经知道如何使用 Redux(不是 React-Redux)。
一、准备环境
为了更好的解读源码,我们可以把源码拷贝到本地,然后搭建一个开发环境。Redux 的使用不依赖于 React,所以你完全可以在一个极为简单的 JavaScript 项目中使用它。这里不再赘述开发环境的搭建过程,需要的同学可以直接拷贝我的代码到本地,然后安装依赖,运行项目。
$ git clone https://github.com/zhongdeming428/redux && cd redux $ npm i $ npm run dev 复制代码
二、阅读源码
(1)源代码结构
忽略项目中的那些说明文档什么的,只看 src 这个源文件目录,其结构如下:
src ├── applyMiddleware.js // 应用中间件的 API ├── bindActionCreators.js // 转换 actionCreators 的 API ├── combineReducers.js // 组合转换 reducer 的 API ├── compose.js // 工具函数,用于嵌套调用中间件 ├── createStore.js // 入口函数,创建 store 的 API ├── index.js // redux 项目的入口文件,用于统一暴露所有 API ├── test │ └── index.js // 我所创建的用于调试的脚本 └── utils // 专门放 工具 函数的目录 ├── actionTypes.js // 定义了一些 redux 预留的 action type ├── isPlainObject.js // 用于判断是否是纯对象 └── warning.js // 用于抛出合适的警告信息 复制代码
可以看出来 redux 的源码结构简单清晰明了,几个主要的(也是仅有的) API 被尽可能的分散到了单个的文件模块中,我们只需要挨个的看就行了。
(2)index.js
上一小节说到 index.js 是 redux 项目的入口文件,用于暴露所有的 API,所以我们来看看代码:
import createStore from './createStore' import combineReducers from './combineReducers' import bindActionCreators from './bindActionCreators' import applyMiddleware from './applyMiddleware' import compose from './compose' import warning from './utils/warning' import __DO_NOT_USE__ActionTypes from './utils/actionTypes' // 不同的 API 写在不同的 js 文件中,最后通过 index.js 统一导出。 // 这个函数用于判断当前代码是否已经被打包工具(比如 Webpack)压缩过,如果被压缩过的话, // isCrushed 函数的名称会被替换掉。如果被替换了函数名但是 process.env.NODE_ENV 又不等于 production // 的时候,提醒用户使用生产环境下的精简代码。 function isCrushed() {} if ( process.env.NODE_ENV !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed' ) { warning( 'You are currently using minified code outside of NODE_ENV === "production". ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.' ) } // 导出主要的 API。 export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes } 复制代码
我删除了所有的英文注释以减小篇幅,如果大家想看原来的注释,可以去 redux 的项目查看源码。
可以看到在程序的头部引入了所有的 API 模块以及工具函数,然后在底部统一导出了。这一部分比较简单,主要是 isCrushed 函数有点意思。作者用这个函数来判断代码是否被压缩过(判断函数名是否被替换掉了)。
这一部分也引用到了工具函数,由于这几个函数比较简单,所以可以先看看它们是干嘛的。
(3)工具函数
除了 compose 函数以外,所有的工具函数都被放在了 utils 目录下。
actionTypes.js
这个工具模块定义了几种 redux 预留的 action type,包括 reducer 替换类型、reducer 初始化类型和随机类型。下面上源码:
// 定义了一些 redux 保留的 action type。 // 随机字符串确保唯一性。 const randomString = () => Math.random() .toString(36) .substring(7) .split('') .join('.') const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` } export default ActionTypes 复制代码
可以看出就是返回了一个 ActionTypes 对象,里面包含三种类型:INIT、REPLACE 和 PROBE_UNKNOW_ACTION。分别对应之前所说的几种类型,为了防止和用户自定义的 action type 相冲突,刻意在 type 里面加入了随机值。在后面的使用中,通过引入 ActionType 对象来进行对比。
isPlainObject.js
这个函数用于判断传入的对象是否是纯对象,因为 redux 要求 action 和 state 是一个纯对象,所以这个函数诞生了。
上源码:
/** * 判断一个参数是否是纯对象,纯对象的定义就是它的构造函数为 Object。 * 比如: { name: 'isPlainObject', type: 'funciton' }。 * 而 isPlainObject 这个函数就不是纯对象,因为它的构造函数是 Function。 * @param {any} obj 要检查的对象。 * @returns {boolean} 返回的检查结果,true 代表是纯对象。 */ export default function isPlainObject(obj) { if (typeof obj !== 'object' || obj === null) return false let proto = obj // 获取最顶级的原型,如果就是自身,那么说明是纯对象。 while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto } 复制代码
warning.js
这个函数用于抛出适当的警告,没啥好说的。
/** * Prints a warning in the console if it exists. * * @param {String} message The warning message. * @returns {void} */ export default function warning(message) { /* eslint-disable no-console */ if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message) } /* eslint-enable no-console */ try { // This error was thrown as a convenience so that if you enable // "break on all exceptions" in your console, // it would pause the execution at this line. throw new Error(message) } catch (e) {} // eslint-disable-line no-empty } 复制代码
compose.js
这个函数用于嵌套调用中间件(middleware),进行初始化。
/** * 传入一系列的单参数函数作为参数(funcs 数组),返回一个新的函数,这个函数可以接受 * 多个参数,运行时会将 funcs 数组中的函数从右至左进行调用。 * @param {...Function} funcs 一系列中间件。 * @returns {Function} 返回的结果函数。 * 从右至左调用,比如: compose(f, g, h) 将会返回一个新函数 * (...args) => f(g(h(...args))). */ export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } // 通过 reduce 方法迭代。 return funcs.reduce((a, b) => (...args) => a(b(...args))) } 复制代码
(4)createStore.js
看完了工具函数和入口函数,接下来就要正式步入主题了。我们使用 redux 的重要一步就是通过 createStore 方法创建 store。那么接下来看看这个方法是怎么创建 store 的,store 又是个什么东西呢?
我们看源码:
import $$observable from 'symbol-observable' // 后面会讲。 import ActionTypes from './utils/actionTypes' // 引入一些预定义的保留的 action type。 import isPlainObject from './utils/isPlainObject' // 判断一个对象是否是纯对象。 // 使用 redux 最主要的 API,就是这个 createStore,它用于创建一个 redux store,为你提供状态管理。 // 它接受三个参数(第二三个可选),第一个是 reducer,用于改变 redux store 的状态;第二个是初始化的 store, // 即最开始时候 store 的快照;第三个参数是由 applyMiddleware 函数返回的 enhancer 对象,使用中间件必须 // 提供的参数。 export default function createStore(reducer, preloadedState, enhancer) { // 下面这一段基本可以不看,它们是对参数进行适配的。 /*************************************参数适配****************************************/ if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { // 如果传递了多个 enhancer,抛出错误。 throw new Error( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function' ) } // 如果没有传递默认的 state(preloadedState 为函数类型,enhancer 为未定义类型),那么传递的 // preloadedState 即为 enhancer。 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { // 如果 enhancer 为不为空且非函数类型,报错。 throw new Error('Expected the enhancer to be a function.') } // 使用 enhancer 对 createStore 进行处理,引入中间件。注意此处没有再传递 enhancer 作为参数。实际上 enhancer 会对 createStore 进行处理,然后返回一个实际意义上的 createStore 用于创建 store 对象,参考 applyMiddleware.js。 return enhancer(createStore)(reducer, preloadedState) } // 如果 reducer 不是函数类型,报错。 if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } /*********************************************************************************/ // 在函数内部定义一系列局部变量,用于存储数据。 let currentReducer = reducer // 存储当前的 reducer。 let currentState = preloadedState // 用于存储当前的 store,即为 state。 let currentListeners = [] // 用于存储通过 store.subscribe 注册的当前的所有订阅者。 let nextListeners = currentListeners // 新的 listeners 数组,确保不直接修改 listeners。 let isDispatching = false // 当前状态,防止 reducer 嵌套调用。 // 顾名思义,确保 nextListeners 可以被修改,当 nextListeners 与 currentListeners 指向同一个数组的时候 // 让 nextListeners 成为 currentListeners 的副本。防止修改 nextListeners 导致 currentListeners 发生变化。 // 一开始我也不是很明白为什么会存在 nextListeners,因为后面 dispatch 函数中还是直接把 nextListeners 赋值给了 currentListeners。 // 直接使用 currentListeners 也是可以的。后来去 redux 的 repo 搜了搜,发现了一个 issue(https://github.com/reduxjs/redux/issues/2157) 讲述了这个做法的理由。 // 提交这段代码的作者的解释(https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b)是防止 Array.slice 的滥用,只有在必要的时候调用 Array.slice 方法来复制 listeners。 // 以前的做法是每次 dispatch 都要 slice 一次,导致了性能的降低吧。 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } // 返回 currentState,即 store 的快照。 function getState() { // 防止在 reducer 中调用该方法,reducer 会接受 state 参数。 if (isDispatching) { throw new Error( 'You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.' ) } return currentState } // store.subscribe 函数,订阅 dispatch。 function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected the listener to be a function.') } // 不允许在 reducer 中进行订阅。 if (isDispatching) { throw new Error( 'You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' ) } let isSubscribed = true // 每次操作 nextListeners 之前先确保可以修改。 ensureCanMutateNextListeners() // 存储订阅者的注册方法。 nextListeners.push(listener) // 返回一个用于注销当前订阅者的函数。 return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error( 'You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' ) } isSubscribed = false // 每次操作 nextListeners 之前先确保可以修改。 ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } // store.dispatch 函数,用于触发 reducer 修改 state。 function dispatch(action) { if (!isPlainObject(action)) { // action 必须是纯对象。 throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { // 每个 action 必须包含一个 type 属性,指定修改的类型。 throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } // reducer 内部不允许派发 action。 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { // 调用 reducer 之前,先将标志位置一。 isDispatching = true // 调用 reducer,返回的值即为最新的 state。 currentState = currentReducer(currentState, action) } finally { // 调用完之后将标志位置 0,表示 dispatch 结束。 isDispatching = false } // dispatch 结束之后,执行所有订阅者的函数。 const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } // 返回当前所使用的 action,这一步是中间件嵌套使用的关键,很重要。 return action } // 一个比较新的 API,用于动态替换当前的 reducers。适用于按需加载,代码拆分等场景。 function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } // 执行默认的 REPLACE 类型的 action。在 combineReducers 函数中有使用到这个类型。 currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) } // 这是为了适配 ECMA TC39 会议的一个有关 Observable 的提案(参考https://github.com/tc39/proposal-observable)所写的一个函数。 // 作用是订阅 store 的变化,适用于所有实现了 Observable 的类库(主要是适配 RxJS)。 // 我找到了引入这个功能的那个 commit:https://github.com/reduxjs/redux/pull/1632。 function observable() { // outerSubscribe 即为外部的 subscribe 函数。 const outerSubscribe = subscribe // 返回一个纯对象,包含 subscribe 方法。 return { subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } // 用于给 subscribe 注册的函数,严格按照 Observable 的规范实现,observer 必须有一个 next 属性。 function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, // $$observable 即为 Symbol.observable,也属于 Observable 的规范,返回自身。 [$$observable]() { return this } } } // 初始化时 dispatch 一个 INIT 类型的 action,校验各种情况。 dispatch({ type: ActionTypes.INIT }) // 返回一个 store 对象。 return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } } 复制代码
不难发现,我们的 store 对象就是一个纯 JavaScript 对象。包含几个属性 API,而我们的 state 就存储在 createStore 这个方法内部,是一个局部变量,只能通过 getState 方法访问到。这里实际上是对闭包的利用,所有我们操作的 state 都存储在 getState 方法内部的一个变量里面。直到我们的程序结束(或者说 store 被销毁),createStore 方法才会被回收,里面的变量才会被销毁。
而 subscribe 方法就是对观察者模式的利用(注意不是发布订阅模式,二者有区别,不要混淆),我们通过 subscribe 方法注册我们的函数,我们的函数会给存储到 createStore 方法的一个局部变量当中,每次 dispatch 被调用之后,都会遍历一遍 currentListeners,依次执行其中的方法,达到我们订阅的要求。
(5)combineReducers.js
了解了 createStore 到底是怎么一回事,我们再来看看 combineReducers 到底是怎么创建 reducer 的。
我们写 reducer 的时候,实际上是在写一系列函数,然后整个到一个对象的属性上,最后传给 combineReducers 进行处理,处理之后就可以供 createStore 使用了。
例如:
// 创建我们的 reducers。 const _reducers = { items(items = [], { type, payload }) { if (type === 'ADD_ITEMS') items.push(payload); return items; }, isLoading(isLoading = false, { type, payload }) { if (type === 'IS_LOADING') return true; return false; } }; // 交给 combineReducers 处理,适配 createStore。 const reducers = combineReducers(_reducers); // createStore 接受 reducers,创建我们需要的 store。 const store = createStore(reducers); 复制代码
那么 combineReducers 对我们的 reducers 对象进行了哪些处理呢?
下面的代码比较长,希望大家能有耐心。
import ActionTypes from './utils/actionTypes' import warning from './utils/warning' import isPlainObject from './utils/isPlainObject' /** * 用于获取错误信息的工具函数,如果调用你所定义的某个 reducer 返回了 undefined,那么就调用这个函数 * 抛出合适的错误信息。 * * @param {String} key 你所定义的某个 reducer 的函数名,同时也是 state 的一个属性名。 * * @param {Object} action 调用 reducer 时所使用的 action。 */ function getUndefinedStateErrorMessage(key, action) { const actionType = action && action.type const actionDescription = (actionType && `action "${String(actionType)}"`) || 'an action' return ( `Given ${actionDescription}, reducer "${key}" returned undefined. ` + `To ignore an action, you must explicitly return the previous state. ` + `If you want this reducer to hold no value, you can return null instead of undefined.` ) } /** * 工具函数,用于校验未知键,如果 state 中的某个属性没有对应的 reducer,那么返回报错信息。 * 对于 REPLACE 类型的 action type,则不进行校验。 * @param {Object} inputState * @param {Object} reducers * @param {Object} action * @param {Object} unexpectedKeyCache */ function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { const reducerKeys = Object.keys(reducers) const argumentName = action && action.type === ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer' // 如果 reducers 长度为 0,返回对应错误信息。 if (reducerKeys.length === 0) { return ( 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.' ) } if (!isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] `". Expected argument to be an object with the following ` + // 返回的是 inputState 的类型。 `keys: "${reducerKeys.join('", "')}"` ) } // 获取所有 State 有而 reducers 没有的属性,加入到 unexpectedKeysCache。 const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) // 加入到 unexpectedKeyCache。 unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) // 如果是 REPLACE 类型的 action type,不再校验未知键,因为按需加载的 reducers 不需要校验未知键,现在不存在的 reducers 可能下次就加上了。 if (action && action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length > 0) { return ( `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + `Expected to find one of the known reducer keys instead: ` + `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` ) } } /** * 用于校验所有 reducer 的合理性:传入任意值都不能返回 undefined。 * @param {Object} reducers 你所定义的 reducers 对象。 */ function assertReducerShape(reducers) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] // 获取初始化时的 state。 const initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === 'undefined') { throw new Error( `Reducer "${key}" returned undefined during initialization. ` + `If the state passed to the reducer is undefined, you must ` + `explicitly return the initial state. The initial state may ` + `not be undefined. If you don't want to set a value for this reducer, ` + `you can use null instead of undefined.` ) } // 如果初始化校验通过了,有可能是你定义了 ActionTypes.INIT 的操作。这时候重新用随机值校验。 // 如果返回 undefined,说明用户可能对 INIT type 做了对应处理,这是不允许的。 if ( typeof reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() }) === 'undefined' ) { throw new Error( `Reducer "${key}" returned undefined when probed with a random type. ` + `Don't try to handle ${ ActionTypes.INIT } or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined, but can be null.` ) } }) } // 把你所定义的 reducers 对象转化为一个庞大的汇总函数。 // 可以看出,combineReducers 接受一个 reducers 对象作为参数, // 然后返回一个总的函数,作为最终的合法的 reducer,这个 reducer // 接受 action 作为参数,根据 action 的类型遍历调用所有的 reducer。 export default function combineReducers(reducers) { // 获取 reducers 所有的属性名。 const reducerKeys = Object.keys(reducers) const finalReducers = {} // 遍历 reducers 的所有属性,剔除所有不合法的 reducer。 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') { // 将 reducers 中的所有 reducer 拷贝到新的 finalReducers 对象上。 finalReducers[key] = reducers[key] } } // finalReducers 是一个纯净的经过过滤的 reducers 了,重新获取所有属性名。 const finalReducerKeys = Object.keys(finalReducers) let unexpectedKeyCache // unexpectedKeyCache 包含所有 state 中有但是 reducers 中没有的属性。 if (process.env.NODE_ENV !== 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { // 校验所有 reducer 的合理性,缓存错误。 assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } // 这就是返回的新的 reducer,一个纯函数。每次 dispatch 一个 action,都要执行一遍 combination 函数, // 进而把你所定义的所有 reducer 都执行一遍。 return function combination(state = {}, action) { if (shapeAssertionError) { // 如果有缓存的错误,抛出。 throw shapeAssertionError } if (process.env.NODE_ENV !== 'production') { // 非生产环境下校验 state 中的属性是否都有对应的 reducer。 const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false // state 是否改变的标志位。 const nextState = {} // reducer 返回的新 state。 for (let i = 0; i < finalReducerKeys.length; i++) { // 遍历所有的 reducer。 const key = finalReducerKeys[i] // 获取 reducer 名称。 const reducer = finalReducers[key] // 获取 reducer。 const previousStateForKey = state[key] // 旧的 state 值。 const nextStateForKey = reducer(previousStateForKey, action) // 执行 reducer 返回的新的 state[key] 值。 if (typeof nextStateForKey === 'undefined') { // 如果经过了那么多校验,你的 reducer 还是返回了 undefined,那么就要抛出错误信息了。 const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey // 把返回的新值添加到 nextState 对象上,这里可以看出来,你所定义的 reducer 的名称就是对应的 state 的属性,所以 reducer 命名要规范! hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 检验 state 是否发生了变化。 } // 根据标志位返回对应的 state。 return hasChanged ? nextState : state } } 复制代码
(6)applyMiddleware.js
combineReducers 方法代码比较多,但是实际逻辑还是很简单的,接下来这个函数代码不多,但是逻辑要稍微复杂一点,它就是应用中间件的 applyMiddleware 函数。这个函数的思想比较巧妙,值得学习。
import compose from './compose' // 用于应用中间件的函数,可以同时传递多个中间件。中间件的标准形式为: // const middleware = store => next => action => { /*.....*/ return next(action); } export default function applyMiddleware(...middlewares) { // 返回一个函数,接受 createStore 作为参数。args 参数即为 reducer 和 preloadedState。 return createStore => (...args) => { // 在函数内部调用 createStore 创建一个 store 对象,这里不会传递 enhancer,因为 applyMiddleware 本身就是在创建一个 enhancer,然后给 createStore 调用。 // 这里实际上是通过 applyMiddleware 把 store 的创建推迟了。为什么要推迟呢?因为要利用 middleWares 做文章,先初始化中间件,重新定义 dispatch,然后再创建 store,这时候创建的 store 所包含的 dispatch 方法就区别于不传递 enhancer 时所创建的 dispatch 方法了,其中包含了中间件所定义的一些逻辑,这就是为什么中间件可以干预 dispatch 的原因。 const store = createStore(...args) // 这里对 dispatch 进行了重新定义,不管传入什么参数,都会报错,这样做的目的是防止你的中间件在初始化的时候就 // 调用 dispatch。 let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) // 注意最后 dispatch 的时候不会访问上面报错的那个 dispatch 函数了,因为那个函数被下面的 dispatch 覆盖了。 } // 对于每一个 middleware,都传入 middlewareAPI 进行调用,这就是中间件的初始化。 // 初始化后的中间件返回一个新的函数,这个函数接受 store.dispatch 作为参数,返回一个替换后的 dispatch,作为新的 // store.dispatch。 const chain = middlewares.map(middleware => middleware(middlewareAPI)) // compose 方法把所有中间件串联起来调用。用最终结果替换 dispatch 函数,之后所使用的所有 store.dispatch 方法都已经是 // 替换了的,加入了新的逻辑。 dispatch = compose(...chain)(store.dispatch) // 初始化中间件以后,把报错的 dispatch 函数覆盖掉。 /** * middle 的标准形式: * const middleware = ({ getState, dispatch }) => next => action => { * // .... * return next(action); * } * 这里 next 是经过上一个 middleware 处理了的 dispatch 方法。 * next(action) 返回的仍然是一个 dispatch 方法。 */ return { ...store, dispatch // 全新的 dispatch。 } } } 复制代码
代码量真的很少,但是真的很巧妙,这里有几点很关键:
-
compose 方法利用
Array.prototype.reduce
实现中间件的嵌套调用,返回一个全新的函数,可以接受新的参数(上一个中间件处理过的 dispatch),最终返回一个全新的包含新逻辑的 dispatch 方法。你看 middleware 经过初始化后返回的函数的格式:next => action => { return next(action); } 复制代码
其中
next
可以看成dispatch
,这不就是接受一个 dispatch 作为参数,然后返回一个新的 dispatch 方法吗?原因就是我们可以认为接受 action 作为参数,然后触发 reducer 更改 state 的所有函数都是 dispatch 函数。 -
middleware 中间件经过初始化以后,返回一个新函数,它接受 dispatch 作为参数,然后返回一个新的 dispatch 又可以供下一个 middleware 调用,这就导致所有的 middleware 可以嵌套调用了!而且最终返回的结果也是一个 dispatch 函数。
最终得到的 dispatch 方法,是把原始的 store.dispatch 方法传递给最后一个 middleware,然后层层嵌套处理,最后经过第一个 middleware 处理过以后所返回的方法。所以我们在调用应用了中间件的 dispatch 函数时,从左至右的经过了 applyMiddleware 方法的所有参数(middleware)的处理。这有点像是包裹和拆包裹的过程。
-
为什么 action 可以经过所有的中间件处理呢?我们再来看看中间件的基本结构:
({ dispatch, getState }) => next => action => { return next(action); } 复制代码
我们可以看到 action 进入函数以后,会经过 next 的处理,并且会返回结果。next 会返回什么呢?因为第一个 next 的值就是 store.dispatch,所以看看 store.dispatch 的源码就知道了。
function dispatch(action) { // 省略了一系列操作的代码…… // 返回当前所使用的 action,这一步是中间件嵌套使用的关键。 return action } 复制代码
没错,store.dispatch 最终返回了 action,由于中间件嵌套调用,所以每个 next 都返回 action,然后又可以供下一个 next 使用,环环相扣,十分巧妙。
这部分描述的有点拗口,语言捉急但又不想画图,各位还是自己多想想好了。
(7)bindActionCreators.js
这个方法没有太多好说的,主要作用是减少大家 dispatch reducer 所要写的代码,比如你原来有一个 action:
const addItems = item => ({ type: 'ADD_ITEMS', payload: item }); 复制代码
然后你要调用它的时候:
store.dispatch(addItems('item value')); 复制代码
如果你使用 bindActionCreators:
const _addItems = bindActionCreators(addItems, store.dispatch); 复制代码
当你要 dispatch reducer 的时候:
_addItems('item value'); 复制代码
这样就减少了你要写的重复代码,另外你还可以把所有的 action 写在一个对象里面传递给 bindActionCreators,就像传递给 combineReducers 的对象那样。
下面看看源码:
/** * 该函数返回一个新的函数,调用新的函数会直接 dispatch ActionCreator 所返回的 action。 * 这个函数是 bindActionCreators 函数的基础,在 bindActionCreators 函数中会把 actionCreators 拆分成一个一个 * 的 ActionCreator,然后调用 bindActionCreator 方法。 * @param {Function} actionCreator 一个返回 action 纯对象的函数。 * @param {Function} dispatch store.dispatch 方法,用于触发 reducer。 */ function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } } // 接受一个 actionCreator(或者一个 actionCreators 对象)和一个 dispatch 函数作为参数, // 然后返回一个函数或者一个对象,直接执行这个函数或对象中的函数可以让你不必再调用 dispatch。 export default function bindActionCreators(actionCreators, dispatch) { // 如果 actionCreators 是一个函数而非对象,那么直接调用 bindActionCreators 方法进行转换,此时返回 // 结果也是一个函数,执行这个函数会直接 dispatch 对应的 action。 if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } // actionCreators 既不是函数也不是对象,或者为空时,抛出错误。 if (typeof actionCreators !== 'object' || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` ) } // 如果 actionCreators 是一个对象,那么它的每一个属性就应该是一个 actionCreator,遍历每一个 actionCreator, // 使用 bindActionCreator 进行转换。 const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] // 把转换结果绑定到 boundActionCreators 对象,最后会返回它。 if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators } 复制代码
这部分挺简单的,主要作用在于把 action creator 转化为可以直接使用的函数。
三、中间件
看了源码以后,觉得中间件并没有想象中的那么晦涩难懂了。就是一个基本的格式,然后你在你的中间件里面可以为所欲为,最后调用固定的方法,返回固定的内容就完事了。这就是为什么大多数 redux middleware 的源码都很短小精悍的原因。
看看 redux-thunk 的源码:
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk; 复制代码
是不是很短很小?那么到底干了什么让它这么受欢迎呢?
实际上 redux-thunk 可以被认为就是:
// 这就是典型的 middleware 格式。 ({ dispatch, getState }) => next => action => { // next 就是 dispatch 方法。注释所在的函数就是返回的新的 dispatch。 // 先判断一下 action 是不是一个函数。 if (typeof action === 'function') { // 如果是函数,调用它,传递 dispatch,getState 和 多余的参数作为 aciton 的参数。 return action(dispatch, getState, extraArgument); } // 如果 action 不是函数,直接 nextr调用 action,返回结果就完事儿了。 return next(action); }; 复制代码
怎么样,是不是很简单?它干的事就是判断了一下 action 的类型,如果是函数就调用,不是函数就用 dispatch 来调用,很简单。
但是它实现的功能很实用,允许我们传递函数作为 store.dispatch 的参数,这个函数的参数应该是固定的,必须符合上面代码的要求,接受 dispatch、getState作为参数,然后这个函数应该返回实际的 action。
我们也可以写一个自己的中间件了:
({ dispatch, getState }) => next => action => { return action.then ? action.then(next) : next(action); } 复制代码
这个中间件允许我们传递一个 Promise 对象作为 action,然后会等 action 返回结果(一个真正的 action)之后,再进行 dispatch。
当然由于 action.then() 返回的不是实际上的 action(一个纯对象),所以这个中间件可能没法跟其他中间件一起使用,不然其他中间件接受不到 action 会出问题。这只是个示例,用于说明中间件没那么复杂,但是我们可以利用中间件做很多事情。
如果想要了解更加复杂的 redux 中间件,可以参考:
四、总结
- Redux 精妙小巧,主要利用了闭包和观察者模式,然后运用了职责链、适配器等模式构建了一个 store 王国。store 拥有自己的领土,要想获取或改变 store 里面的内容,必须通过 store 的各个函数来实现。
- Redux 相比于 Vuex 而言,代码量更小,定制化程度更低,这就导致易用性低于 Vuex,但是可定制性高于 Vuex。这也符合 Vue 和 React 的风格。
- Redux 源码比较好懂,读懂源码更易于掌握 Redux 的使用方法,不要被吓倒。
- Redux 中间件短小精悍,比较实用。如果从使用方法开始学中间件比较难懂的话,可以尝试从源码学习中间件。
最后,时间紧迫,水平有限,难免存在纰漏或错误,请大家多多包涵、多多指教、共同进步。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- AQS源码详细解读
- SDWebImage源码解读《一》
- MJExtension源码解读
- axios 核心源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
未来世界的幸存者
阮一峰 / 人民邮电出版社 / 2018-6-1 / 39.00 元
本书为阮一峰博客文集,主要收录的是作者对技术变革的影响的一些思考,希望能够藉此书让读者意识到世界正在剧烈变化,洪水就在不远处,从而早早准备出路。本书适合所有乐于思考的读者。一起来看看 《未来世界的幸存者》 这本书的介绍吧!