Redux 源码拾遗
首先来一段 redux 结合 中间件 thunk、logger 的使用demo 了解一下应该如何使用
const redux = require('redux') const { createStore, combineReducers, bindActionCreators, compose, applyMiddleware } = redux const simpleState = { num: 0 } const complexState = { num: 1 } // logger中间件负责打印日志 function logger({dispatch, getState}) { return function(next) { return function(action) { console.log('logger start:' + action.type) const res = next(action) console.log('logger end:' + action.type) return res } } } // thunk中间件负责异步 function thunk({dispatch, getState}) { return function(next) { return function(action) { if (typeof action === 'function') { return action(dispatch) } return next(action) } } } // 派发的增加方法 function increment(num) { return { type: 'increment', payload: num } } // 派发的减少的方法 function decrement(num) { return { type: 'decrement', payload: num } } // 派发的乘法的方法 function multiply(num) { return { type: 'multiply', payload: num } } // 派发的除法的方法 function divide(num) { return { type: 'divide', payload: num } } // 派发异步事件方法 function syncDivide() { return (dispatch) => { setTimeout(() => { dispatch(divide(10)) }, 2000) } } // 负责加减法的reducer function simpleCalcReducer(state = simpleState, action) { switch (action.type) { case 'increment': return Object.assign({}, state, { num: state.num + action.payload }) case 'decrement': return Object.assign({}, state, { num: state.num - action.payload }) default: return state } } // 负责乘除法的reducer function complexCalcReducer(state = complexState, action) { switch (action.type) { case 'multiply': return Object.assign({}, state, { num: state.num * action.payload }) case 'divide': return Object.assign({}, state, { num: state.num / action.payload }) default: return state } } const middleware = applyMiddleware(thunk, logger) const allReducers = combineReducers({ simpleCalcReducer, complexCalcReducer }) const store = createStore(allReducers, middleware) // 所有action const allActions = { increment, decrement, multiply, divide, syncDivide } const actions = bindActionCreators(allActions, store.dispatch) // 派发action actions.increment(2); actions.decrement(1); actions.multiply(10); actions.divide(5); actions.syncDivide() setTimeout(() => { console.log(store.getState()) }, 2500)
打印结果如下:
上面只是简单使用了redux的部分api,来尽量实现一个标准项目中所使用到的redux的基本操作流程,下面进入源码的分析阶段
1.项目结构
第一张图,先来看一下 redux 的项目结构目录
---src
------utils // 工具函数库
---------actionTypes.js // redux用来初始化state,自身调用的action
---------isPlainObject.js // 用来检测action 是否为一个明确的对象
---------warning.js // 用来进行警告的公共方法
------applyMiddleware.js // 中间件,负责对store.dispatch方法进行包装
------bindActionCreators.js // 对派发action的语法进行优化,使之可以更好的应用于react或其他
------combineReducers.js // 将分离的reducer合并成一个对象,并返回一个执行这些reducer的函数
------compose.js // 合并函数, 将函数参数按照从左向右的顺序进行合并,返回一个新函数
------createStore.js // 核心方法,返回一个store对象,用来获取state、派发action、添加订阅者subscribe 等
------index.js // 入口文件,对外导出的方法
2.utils 工具方法的分析
将utils的三个方法放在最前面来分析,主要是为了先对三个工具方法有个简单概念和了解,这样在阅读下面的内容时,对于有用到工具方法的地方可以直接理解,不用再打断思绪去分析工具方法,影响整体的阅读体验
① warning.js -- 控制台打印异常日志
export default function warning(message) { if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message) } try { throw new Error(message) } catch (e) {} }
总结:
warining 函数非常简单,当 console.error 方法存在时来打印 错误的 message,这里的一层判断是为了兼容ie6789浏览器只有在开启控制台的情况下,console对象才会创建,否则会报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 ”,翻译过来就是,抛出这个错误是为了方便查看错误源。只要我们开启了断点异常,那么程序就会在抛出错误的那行代码上打上断点,来方便进行调试和追踪。那么在谷歌里面这个异常怎么开启呢?F12打开谷歌的开发者工具,1点击 Sources - 2点击蓝色的pause icon - 3勾选 Pause on caught exceptions,如下图所示
在控制台里测试如下代码
键入回车后,浏览器出现断点,跳转至sources资源文件,并高亮了抛出错误的那行代码,非常方便
② isPlainObject.js -- 判断目标是否为明确的对象
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 }
总结:
Object.getPrototypeOf 方法用来返回对象的隐式原型,即构造这个对象的构造函数的原型。
使用 while 循环来查找目标对象的原型链顶层,因为 Object.prototype.__proto__ === null,所以Object.prototype 就是原型链的顶层,查找到最后一层时,proto 一定被赋值为 Object.prototype。
这样做的意义是什么? Array数据类型 或者 dom的 document等数据类型的typeof 也是 object, 他们都不是直接由Object 函数来构建的对象,以 Array 数组的构造函数来举例。
var ary = new Array() var pro = Object.getPrototypeOf(ary) console.log(pro === ary.__proto__) // true console.log(ary.__proto__ === Array.prototype) // true var pro2 = Object.getPrototypeOf(pro) console.log(pro2 === Object.prototype) // true Object.prototype.__proto__ === null // true
可见 ary 第一次获取到的原型是 Array.prototype,而 Array.prototype 本身也是一个对象,必然由 Object 函数创建,所以 Array.prototype.__proto__ 又指向了 Object.prototype,到此循环结束。最终 pro = Object.prototype。这就造成了最终的 Object.getPrototypeOf(obj) 和 proto 是不等的。
所以这个方法的就很清晰了,只有直接使用 Object 函数创建的对象才会被判断为真,因为只有它原型链存在一层
③ actionTypes.js -- redux 内部做初始化state的action
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
总结:
randomString() 方法随机生成一个36进制的数字,然后切割拼接,最终生成 和"5.b.p.3.b.8" 格式一样的字符串
这个方法 导出了一个对象,对象包含3个key: INIT、REPLACE、PROBE_UNKNOWN_ACTION,前两个是字符串,后面一个是方法,方法也是返回一个拼接好的字符串,其实这三个都是redux内部用来派发action 的 type
3.模块分析(去掉注释)
① 入口文件 index.js**
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' 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.' ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }
总结:
入口文件作用就时将几个模块文件引入和导出,里面有一个空的 isCrushed 函数,这个函数的意义就是判断当前的构建工具是否为 node 环境,如果是 node 环境并且非生产环境,那么就要判断当前的 redux 文件有没有被压缩。
判断的目的就是希望开发者,在非生产环境下不要压缩代码,如果项目比较大,我们只是修改了一个小小的样式,这时候如果开启代码的混淆压缩,那么我们项目的所有依赖的文件都会被混淆压缩,项目越大被压缩的内容越多耗时越长,从而导致调试的时间增加,降低开发效率。这也正是redux在 warning 警告里提到的 'This means that you are running a slower development build of Redux',你正在一个缓慢的开发环境下使用 redux。
② 核心文件 creteStore.js
cretaeStore.js 是redux的核心文件,在这个方法里,redux 向外提供了 dispatch、subscribe、getState、replaceReducer 这四个核心方法。此外还有一个 [$$observable] 方法,这个方法并不是很好理解他的作用和意义,放在文章最后来说明。下面是移除了注释的源代码
import $$observable from 'symbol-observable' import ActionTypes from './utils/actionTypes' import isPlainObject from './utils/isPlainObject' export default function createStore(reducer, preloadedState, enhancer) { // 判断1 if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { 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.' ) } // 判断2 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } // 判断3 if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } // 判断4 if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } // 内部变量 let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false function ensureCanMutateNextListeners() { // ... } function getState() { // ... } function subscribe(listener) { // ... } function dispatch(action) { // ... } function replaceReducer(nextReducer) { // ... } function observable() { // ... } dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
createStore 方法接收3个参数,分别是 reducer,preloadedState,enhancer。
1.reducer 就是一个纯函数用来返回 state
2.preloadedState 是初始化的state,在实际开发中,很少有传递这个这个参数。其实这个参数就是为了初始化必须的原始数据。此外,如果使用了combineReducer这个方法来组合多个reducer,相应的preloadedState 里的key 也必须要和 combineReducer 中的key相对应
3.enhancer 翻译过来就是增强器,它是一个类似的高阶函数用来包装和增强 creteStore 内部的 dispatch、subscribe、getState 等方法,通过这个高阶函数可以实现 中间件、时间旅行、持久化state等,在redux内只实现了一个enhancer,它就是中间件 applyMIddleware,用来强化 dispatch方法。
讲完三个参数,开始解释代码
if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { 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.' ) }
判断1:提示很明确,当使用多个enhancer时,需要使用compose 方法将多个enhancer合并成一个单一的函数。
// 判断2 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined }
判断2:个人感觉这里的判断就是为了符合更大众化的开发需求,preloadedState 这个参数在实际开发中,真的很少传递。所以在这里做了一个判断,如果开发中没有用到这个初始的preloadedState,完全可以将它省略掉,直接传递最后一个enhancer函数,redux在内部帮开发者,完成了这部分参数转换的处理。
// 判断3 if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) }
判断3:当enhancer 存在并且为函数的时候,直接将当前creteStore方法作为参数传递给 enhancer。而这个enhancer(createStore),返回的就是 createStore 的加强版可以称他为 creteStore-X,至于如何增强先放到后面的applyMiddleware这个enhancer来说明。这里先只需要知道,通过enhancer包装过后的 createStore,内部的某些方法被加强了。
// 判断4 if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') }
判断4:reducer 必须是一个函数 函数 函数
let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false
内部变量:
这里分别使用了,currentReducer 和 currentState 来接收初始传递的 reducer 和 preloadedState,重新赋值是因为 currentReducer 和 currentState 都是可变的,当他们被修改的时候不会影响初始的reducer 和 preloadedState。
currentListeners 是一个数组用来存储订阅的函数列表,为什么还要多定义一个 nextListeners = currentListeners 呢?这个问题放到后面看比较好理解,先掠过
isDispatching 比较好理解,用来判断redux是否处于派发action的状态中,即是否在执行reducer。
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }
ensureCanMutateNextListeners:返回currentListeners 的浅拷贝
这里的意图不是很明显,当nextListeners 和 currentListeners 全等的时候,返回一个 currentListeners 的 浅拷贝赋值给 nextListenenrs,意义是什么呢接着向下看。
function getState() { 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 }
getState:返回当前的currentState
返回当前的currentState,当action还在 派发当中时,如果调用这个方法会抛出错误
function subscribe(listener) { // 被添加的订阅者必须是一个函数 if (typeof listener !== 'function') { throw new Error('Expected the listener to be a function.') } // 处于dispatch的时候,不允许添加订阅者 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.' ) } // 这里是一个闭包,isSubscribed 用来表示当前的订阅者已经成功的被 // 添加到了订阅的列表中 let isSubscribed = true // 当nexListeners 和 currentListeners 全等时候,返回了一个新的 // nextListeners, 然后将订阅者添加到新的 nextListeners ensureCanMutateNextListeners() // ?1 nextListeners.push(listener) // 返回一个 unsubscribe, 用来取消已经添加到订阅列表中的订阅者 return function unsubscribe() { // 如果当前的订阅者已经被取消了 直接返回 if (!isSubscribed) { return } // 处于dispatch的时候,不允许取消订阅者 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.' ) } // 将订阅者的状态改为false isSubscribed = false // 继续更新 nextListeners ensureCanMutateNextListeners() // 将取消的订阅者从 nextListeners 中删除 const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
subscribe:用来添加和删除订阅者
这样粗略看下来似乎也没太看清楚这个 ensureCanMutateNextListeners 方法有什么卵用,继续翻看redux 对ensureCanMutateNextListeners 方法的解释,里面提到
"This makes a shallow copy of currentListeners so we can use nexListeners as a temporary list while dispatching"
"This prevents any bugs around consumers calling subscribe/unsubscribe in the middle of a dispatch"
主要就是后面这一句,这个方法 可以防止用户在执行dispatch中调用订阅或者取消订阅时出现任何错误。还是不明所以,继续向下寻找答案
function dispatch(action) { // 判断 action 是否为明确的 object 对象 if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } // action 必须有 type 这个字段类型 if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } // 不允许同时执行多个 dispatch if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } // 将执行状态设置为true, 开始执行reudcer, 并接收 currentState try { isDispatching = true currentState = currentReducer(currentState, action) } finally { // 无论成功失败 都将执行状态重置 isDispatching = false } // reducer 执行完毕之后,开始循环执行 订阅者数组列表 const listeners = (currentListeners = nextListeners) // ?2 for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] // ?3 listener() } return action }
dispatch:用来派发action,执行reducer
问题2,每次执行订阅者列表的时候,为什么要新定义一个 listeners 来接收 nextListeners,直接 currentListeners = nextListeners 然后使用 currentListeners 来循环不行吗?
问题3,循环里面为什么不直接使用 listeners[i]() 来执行调用,而是定义一个变量来接收然后再执行?
再结合上面 subscribe 中 ensureCanMutateNextListeners 的到底有何意义?
其实,这一切的细节都是为了解决一种特殊的应用场景,在订阅事件内部 再次 添加订阅或者取消订阅。如
store.subscribe( () => { store.subscribe(...) })
如果是这种场景,因为是在循环中去触发 listener,单纯的使用 currentListeners 来存储订阅列表是无法满足的。循环还没有结束,其中的某个 listener 对 currentListeners 进行了添加或者删除,都会影响到此次循环的进行,带来不可预期的错误。至此这些细节就变得清晰起来了。
引用官方的注释更为准确,每次dispatch后,循环使用的是当前nextListeners 的快照,同样也就是 currentListeners,它在循环结束之前是不会被改变的。想象一下,假如在订阅事件的内部继续调用 store.subsrcibe 来添加订阅者,那么就会调用 ensureCanMutateNextListeners 这个方法,如果currentListeners 和 nextListeners 是完全相等的说明nextListeners 还未被改变,此时浅拷贝一份 currentListenrs 的队列赋值为 nextListeners,nextListeners 就变成了一个全新的订阅队列,然后将 添加的订阅者放到新的 nextListeners,这样就完全不会影响到之前已经开始的循环。当下次disptach 再次发起的时候,将 currentListeners 同步为最新的 nextListeners 队列。
问题 2 应该如何理解呢?找到了早期redux的提交记录如下:
这里还没有对 currentListeners 和 nextListeners 做概念的区分,只是将每次listeners 浅拷贝了一层用来 安全的执行循环。所以 const listeners = (currentListeners = nextListeners) 中声明的 listeners并不是必须的,他的存在只是为了在之后在循环中使用 listeners 代替 currentListeners 少打几个字母而已
问题 2 应该如何理解呢?其实在这里是为了确保在 listener 当中的 this 与我们对 js 当中的函数内部 this 指向谁的预期保持一致。这里就是将 this 重新绑定为默认的全局对象,如果直接使用 listeners[i]() 来调用,那么其内部的this 变指向了listeners 这个数组本身。
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }
replaceReducer:替换reducer,并且重新 dispatch
replaceReducer 这个方法比较简单,当我们的项目启用了模块懒加载,我们的最开始的reducer可能只是部分的模块的reducer,这时候如果要引入新的模块就需要能够动态的替换 reducer 来更新state。官方的注释里还写道,如果在开发模式下启用了热更新,同样需要这个函数来进行替换。
function observable() { const outerSubscribe = subscribe return { subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } }
observable:此方法返回一个 observable 对象,该对象拥有 subscribe 的方法来添加一个可观测的对象。那么这个方法到底时干什么用的呢?注释里有写道 “For more information, see the observable proposal:https://github.com/tc39/propo...”,打开这个地址看到这个项目是将 可观察类型引入到ECMAScript标准库中,而redux 这里就是实现了 observable 的观察对象。这里不是很清楚如何使用 ... 略过。
dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }
最后使用 dispatch({ type: ActionTypes.INIT }) 生成一个初始化的state,这也就是为什么preloadedState不需要传递,就可以得到初始化的state了。因为redux内部在执行 createStore 这个方法的时候,自动执行了一次 disptach。最后将众方法返回
③ combineReducers -- 将多个reducer 合并成一个函数
import ActionTypes from './utils/actionTypes' import warning from './utils/warning' import isPlainObject from './utils/isPlainObject' // 函数1 function getUndefinedStateErrorMessage(key, action) { // ... } // 函数2 function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { // ... } // 函数3 function assertReducerShape(reducers) { // ... } // 函数4 export default function combineReducers(reducers) { // ... }
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.` ) }
getUndefinedStateErrorMessage: 方法比较简单,返回一段提示信息,提示 reducer不能返回undefined
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' // reducer 不存在时进行的提示 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.' ) } // state 为非明确的对象的时候的提示 if (!isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join('", "')}"` ) } // 当state当中的某个key值无法在reducers当中找到,并且还未被加入到unexpectedKeyCache // 那么就把这个key筛选出来 const unexpectedKeys = Object.keys(inputState).filter( key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] ) // 将上一步骤筛选出来的key,存储到unexpectedKeyCache里面 unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) // 如果是使用了 replaceReducer 替换了reducer,那么就不需要进行提示了,因为之前 // 的state 的数据可能和 新的reducer 不能保持一致 if (action && action.type === ActionTypes.REPLACE) return // 将state里面reducer不能操作的key打印出来 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.` ) } }
getUnexpectedStateShapeWarningMessage: 在非生产环境对finalReducer进行的校验,将state内异常的 key 抛出,进而提示开发者。
function assertReducerShape(reducers) { // 循环所有reducer, 并使用 ActionTypes.INIT 和 ActionTypes.PROBE_UNKNOWN_ACTION() // 两个type类型的 action 校验所有reducer 是否返回了符合规范的 state Object.keys(reducers).forEach(key => { const reducer = reducers[key] 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.` ) } 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.` ) } }) }
assertReducerShape:用来断言reducers返回的结果是不是符合要求的类型,如果不满足判断它会抛出一个error
可以看出在不使用 combineReducers 的时候,我们编写的唯一的reducer是不用这些校验的,如果我们在其中返回了 undefined 那么必然最终的 currentState 就变成了 undefined。
那么为什么 combineReducers 这个方法强制所有的 reduer 不能返回 undefined 呢?
找到了redux的中文文档:http://cn.redux.js.org/docs/a...
里面提到:”combineReducers 函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根 redcuer 时并不需要遵守这些规则“
这样就很清楚了,原来多的这些校验是为了更好的提示新手用户,reducer的正确使用规范。
这里还有一个问题,使用 {type: ActionTypes.INIT} 来校验 reducer 是否返回正确的state,ActionTypes.INIT 应该必然是 reducer 内部未知的 action 了。但是为什么下面还要用 { type: ActionTypes.PROBE_UNKNOWN_ACTION() } 在重复校验一次吗?难道说只是为了说明两次校验的出发目的不一样?但是这样是不是多余了
export default function combineReducers(reducers) { // 接收一个对象形式的 reducers 集合如 { reducer1: () => {}, reducer2: () => {}, } const reducerKeys = Object.keys(reducers) // 最终将要被执行的reducers 集合 const finalReducers = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] // 判断当前如果是非生成环境 并且 reducers[key] 不存在时在控制台打印警告信息 if (process.env.NODE_ENV !== 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } // 只有当 reducers[key] 为函数类型时 才添加到 finalReducers 当中 if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } // 获取finalReducers 当中的 key const finalReducerKeys = Object.keys(finalReducers) // 用来存放state中不能被操作的 key let unexpectedKeyCache if (process.env.NODE_ENV !== 'production') { // 为了优化性能只在非生产模式下进行校验 unexpectedKeyCache = {} } // 用来校验finalReducers中每一个reducer是否合乎规范 let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } return function combination(state = {}, action) { // 到这里开始执行 reducer if (shapeAssertionError) { // 如果reducer返回了undefined直接抛出错误 throw shapeAssertionError } // 非生产环境进行提示 if (process.env.NODE_ENV !== 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } // 判断 state 有没用被更改 let hasChanged = false // 重新生成的state const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === 'undefined') { // 如果reducer 返回了undefined 抛出错误 const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey // 如果state 内的某个key的数据已经被更改过 此处必然是 true hasChanged = hasChanged || nextStateForKey !== previousStateForKey } // 根据state是否改动过 返回对应的state return hasChanged ? nextState : state } }
combineReducers:这个方法就是将多个reducers循环执行后最终返回一个合并后的state,这个方法还是比较简单的。
④-- bindActionCreators,提供了一种调用dispatch的其他方式,代码本身比较简单,这里就不在赘述了
function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } } export default function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } 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"?` ) } const boundActionCreators = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }
⑤ -- compose, 组合函数,将多个函数参数按照传入的顺序组合成一个函数,并返回。第二个参数作为第一个函数的参数,第三个参数作为第二个函数的参数,依次类推。返回的函数接收的参数,将作为compose 初始参数的最后一个函数的参数,源码如下
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } // 最终 compose 返回的是一个 (...args) => a(b(...args)) 这样的函数 // 那么这个函数内部调用的顺序也就清楚了,执行这个函数的时候,首先会执行 // var c = b(...args), 然后将c 作为参数传递给 a,如果多个函数的话依次类推, // 最先执行最里面的那个参数,由里向外 return funcs.reduce((a, b) => (...args) => a(b(...args))) }
问题1,先执行了b(...args)函数,不就直接开始从里面执行了吗?demo1
var funcs2 = [ function a() { console.log('a') }, function b() { console.log('b') }, function c(arg) { console.log(arg) console.log('c') } ] var t = compose(funcs2) t('d') // d c b a
说好的剥洋葱呢,怎么就成半个了?
其实compose这个方法只是提供了组合函数的功能,真正想要实现完整的从外到内这种完整的洋葱模型,还需要对传递到compose参数的每个函数做处理,它的每个函数一定是返回了一个新的函数,这样才能确保它不会被简单的执行,思考下面的demo
var funcs = [ function a(b) { return () => { console.log('a') return b(); } }, function b(c) { return () => { console.log('b'); return c(); } }, function c(arg) { return () => { console.log('c'); console.log(arg) } } ] var f = compose(funcs)('d'); f() // a b c d
这就和文章最开始实现的 logger 和 thunk 的中间件很相似了。a 里执行 b , 然后 b 里执行 c,一个中间件的雏形已经出现了。
⑥ -- applyMiddleware,redux内唯一实现的enhancer,用来扩展dispatch方法,也是redux中最难理解的地方,一睹真容吧,为了便于对执行过程的理解,这里贴一下中间件redux-thunk源码的简化版
({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState); } return next(action); }
import compose from './compose' export default function applyMiddleware(...middlewares) { // 这里 applyMiddleware 返回的函数接收的第一个参数是 creteStore, // 这就是之前 createStore内直接使用 enhancer(createStore)(reducer, preloadedState) // 的原因了。 return createStore => (...args) => { // store 就是最原始的 store 对象,这里还未对store.dispatch 进行扩展 const store = createStore(...args) // 声明一个dispatch的方法,注意这里使用的时let,表示这个dispatch将要会被改变 // 而改变过后的dispatch 就是加入了中间价增强版的 dispatch let dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } // middlewareAPI和字面意思一样就是,中间件需要用到的原始的store对象的方法,, // 这里提供了两个方法,一个getState, 一个是dispatch。等等这里好像有点不一样的东西 // 为什么这里 要写成 dispatch: (...args) => dispatch(...args),而不是直接 // dispatch: dispatch 先留下继续向下看。 const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // 这里参考上面的redux-thunk的源码 chain最终返回的结果就是 /* chain = [ next => action => { if (typeof action === 'function') { return action(dispatch, getState); // 此处的dispatch 和 getState 即为 middlewareAPI的dispatch 和 getState } return next(action); } ] */ const chain = middlewares.map(middleware => middleware(middlewareAPI)) // 结合之前已经分析过的compose,compose(...chain)最后一个参数来接收 store.dispatch // 因为这里只传递了一个thunk中间件,所以,这里的 dispatch 就变成了 /* dispatch = (action) => { if (typeof action === 'function') { return action(dispatch, getState); // 此处的dispatch 和 getState 依然为 middlewareAPI的dispatch 和 getState } // 由于next被赋值为store.dispatch, 此处实际为 return store.dispatch(action); } */ // 也因为这里dispatch 被重新赋值,导致middlewareAPI内dispatch属性 // 里使用到的 dispatch 也变了,不再是抛出error错误的那个函数了。 // 这就是为什么一定要写成 dispatch: (...args) => dispatch(...args) 的原因了 // 如果写成 dispatch: dispatch, 相当于只是初始声明了这个方法,后续dispatch的修改就与它无关了 dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } // 返回的dispatch 替换掉了 store 内部初始的 dispatch, dispatch被扩展了 } }
总结:
通过对源码的翻阅,了解到了整个redux的执行流程和机制。通过createStore来初始化state,当需要使用异步action的时候,可以使用 applyMiddleware 将redux-thunk redux-saga 等中间件对store.dispatch 进行扩展。每次调用 dispatch 都会执行所有的reducer,reducer执行完毕后,会更新所有的订阅事件。