掌握真相,你才能获得自由
redux它很小(不到7k),但是却拥有丰富的(几百个)插件生态系统。这扩展性是多么好啊,才能拥有这么多的插件。本文将详细解读:每行redux及其中间件源码-(版本v4.0.5),不仅让你知道这句代码的含义,更会让你知道为什么这么写?。
1 介绍
redux是什么?
引用官方的话来说:
redux是为js设计的一个可预测的状态管理应用。它能帮助您编写行为一致的应用,它可以运行在不同的环境(客户端,服务端,原生),并且非常容易测试。除此以外,它提供了很棒的开发人员体验,比如实时代码编辑与时间旅行调试器相结合。你可以在react或任何其他视图库中使用。
说白了,redux就是状态管理库-store,可以帮助我们保存一些状态-state,并提供一个方法-dispatch进行修改状态state。与此同时,我们也可以订阅-subscribe状态的变化,在每次状态改变(dispatch调用)后,来发送通知,以便处理相关逻辑。
基本名词
store: 是一个容器,包含状态(state),修改状态的方法(dispatch())以及订阅状态变化的方法(subscribe())等。state: 保存状态相关的数据,只能是普通对象,数组或者原始值,通常为普通对象。action: 动作行为,它是一个普通对象,里面必须有type属性,用来描述当前的行为。dispatch(): 派发器,派发一个动作(action),它是唯一修改状态(state)的方法。reducer(): 处理器,参数为state和action。通过action中不同的type,来返回新的state。subscribe(): 订阅器,接收一个回调函数,每次state改变,都会调用这个函数。
拓展名词
actionType: 指的就是action中的type。actionCreator(): 生成action的函数。纯函数: 1.有相同的输入,必有相同的输出。2.不会有副作用,就是不会改变外面的状态。它是redux可预测功能的保证。
三大核心
单一数据源: 是指应用的全局状态应保存在单一的store中(应用有且只有一个store)。state只读:
state是不能直接被改变,改变它的唯一方式是调用dispatch()方法。用纯函数返回新的状态:这个纯函数指的就是reducer()。
- 单一数据源:可以方便调试和检测;
- 只读的
state:可以防止由于其他任何Modal层或View层,直接修改state,而导致难以复现的bug,从而保证所有内部状态的改变被集中管控,并且严格按照顺序执行; - 再利用纯函数来返回新的状态,我们可以很方便的记录用户的操作,来实现撤销,重做,时间旅行等功能。
注:
- 修改
state只能通过dispatch方法,避免直接修改state的值。如果直接修改state某个属性,属性值虽然会变化,但这个变化并不会被订阅器监听到,这会引起难以复现的bug。 - 强制使用action来描述每次发生的变化,这会使我们清晰的知道应用程序中发生了什么。如果一个状态改变了,也可以知道它为什么改变。
中间件原理
中间件有什么作用呢?它可以扩展我们redux,例如通过中间件,我们可以dispatch一个异步action,可以帮助我们记录state状态变化,以及很容易实现时间旅行等众多功能。
那么redux中间件函数到底是改变的是什么?
答案:就是改造dispatch函数。
通过应用多个中间件函数,每个中间返回的dispatch()(即下图中next())被一层层包裹,像个洋葱一样,如下图:
如图所示,当我分别应用a、b、c三个中间件时,每次我们调用dispatch()方法,这些中间件函数会依次执行,其中最内层就是原始的dispatch()。
带有中间件的任务流
2 createStore()
通过这个函数,我们可以创建一个store。
createStore()可以传入三个参数:
reducer:(必传)是一个函数,根据不同的action,来返回新的state。preloadedState:(可选) 初始化stateenhancer: (可选)是增强函数,通常配合中间件使用。可以是applyMiddleware(中间件函数)
用法如下:
const store = createStore(reducer, preloadedState, enhancer) // 第一种用法
const store = createStore(reducer, enhancer) // 第二种用法 或者,如果不在此函数中设置初始值(而是在reducer函数设置),则可以直接用
reducer()可以接收2个参数:
state: (必传)是初始值action: (必传)是一个对象,包含type(触发行为类型)属性,和与数据相关的属性。
用法如下:
const initState = {
num: 1,
}
const reducer = (state = initState, action) {
const num = state.num
switch (action.type) {
case "add": return {...state, num: num + 1};
case "minus": return {...state, num: num - 1};
default: return state
}
}
createStore()函数主要功能:
- 如果有中间件,将
reducer和state传入中间件函数。 - 初始化
state,reducer,以及初始化收集订阅函数的数组。 - 返回四个方法,如下图:
源码如下:
function createStore(reducer, preloadedState, enhancer) {
// 如果第二参数,第三个参数都是函数
// 或者第三个和第四个参数都是函数会抛出错误。
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.'
)
}
// 如果第二个参数是个函数,第三个参数为undefined,那么说明此时,调用方式为createStore(reducer, enhancer),此时preloadedState就是enhancer。
// 所以需要将enhancer设置为preloadedState,而preloadedState为undefined
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
/**======================================================
下面才是主要内容
=======================================================**/
// 如果传参的形式类似createStore(reducer, preloadedState, {})
// 因为enhancer只能为函数,如果传入不是函数则报错。
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 这句会配合中间件讲解
return enhancer(createStore)(reducer, preloadedState)
}
// 如果传入的reducer不是函数,也会抛出错误
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
// 将传入reducer保存在currentReducer中
let currentReducer = reducer
// 将传入初始化state(preloadedState),保存在currentState
let currentState = preloadedState
// currentListeners和nextListeners保存订阅的回调函数队列
let currentListeners = []
let nextListeners = currentListeners
// 判断是否正在执行dispatch方法
let isDispatching = false
// 定义以下几个方法
function getState(){...}
function subscribe(listener) {...}
function dispatch(action) {...}
function replaceReducer(nextReducer) {...}
// 执行一次dispatch方法,目的为了每个reducer能拿到初始的state。
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
我们先分别介绍各个方法的实现
2.1 getState()
通过这个方法,我们可以取得最新的state。
用法如下:
const newState = store.getState()
源码中getState() 方法非常简单,它会直接返回内部变量currentState,这个变量的值就是state。
/**
* 读取state
*/
function getState() {
if (isDispatching) { // 如果此时正在执行dispatch方法,会抛出错误。
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
}
源码中还做了一个判断,如果当前正在dispatch过程中,会抛出一个错误。
2.2 dispatch()
这个方法是我们唯一修改state的方法。它可以传入一个action,其中的type属性用来描述当前的行为。
用法如下:
const addAction = {type: "add"}
store.dispatch(addAction)
函数内部主要功能:
- 调用
reducer()函数返回新的state - 调用所有订阅的函数,依次执行。
源码如下:
function dispatch(action) {
// 如果不是对象,抛出错误
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// 如果type为undefined抛出错误
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 为了避免在reducer函数中调用dispatch(),这会导致无限循环。因此抛出错误。
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
/**======================================================
下面才是主要内容
=======================================================**/
try {
isDispatching = true
// currentReducer是createStore传入的reducer
// 调用用户传入reducer函数,然后将值赋值给内部变量保存。
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 取出保存的所有订阅的函数,然后执行。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// 将传入action返回
return action
}
思考1:上面代码为什么使用
try...finally,为什么没有使用try...catch?
原因在于,用户传入reducer函数执行有可能会报错,由于没有使用catch,所以这个错误会被抛出,此函数终止运行。这个不难理解。
那finally作用呢?它就是不管有没有报错,都会重置isDispatching,如果有报错,它可以防止接下来所有的dispatch()失效。
思考2:如果同时多次调用
dispatch()方法,那么所有订阅的函数也会多次调用?或者dispatch()为啥不接收一个数组,从而可以派发多个aciton?
如果同时多次调用dispatch()方法,所有订阅的函数也会多次调用,这是肯定得。
dispatch()为啥不接收一个数组作为参数。如果有出现这种问题,那么首先要考虑,你定义的action是否合适?如果完全可以用一个action来解决问题,那么就没有必要使用多个action。但是如果你非要想同时触发多个action,但是只触发一次订阅函数,那么可以使用高阶reducer的方式解决,详见:
社区也有相关的插件
思考3:
dispatch()中为什么没有做判断:如果state改变才会调用所有订阅的函数,这不会提升性能吗?
是的,这会提升性能,但是redux把控制权交给了使用者,由使用者自行定义。如果你需要这样一种场景:每次dispatch()以后,不管state有没有变化,这样都会通知订阅者。那么你要感谢作者将控制权留给了你。
通过下面代码也可以知道: reducer()中不能使用getState(), dispatch(), subscribe()等方法。如果使用则会报错。
try {
isDispatching = true
// 调用用户传入reducer函数,然后将值赋值给内部变量保存。
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
2.3 subscribe()
它可以订阅
state的变化,它接收一个回调函数,这个函数会被添加到内部的队列中保存,state变化以后,回调函数就会被执行。它执行结果,返回一个函数,执行这个函数以后,之前传入的回调函数会从内部的队列中删除,达到取消订阅的目的。
用法如下:
// 订阅
const unsubscribe = store.subscribe(function stateChange() {
console.log("newState:", store.getState())
})
// 取消订阅
unsubscribe()
函数内部主要功能:
- 它会将订阅的回调函数添加到队列中保存。
subscribe()的返回值是一个方法,调用以后可以取消从队列中将回调函数删除。
源码如下:
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#subscribelistener for more details.'
)
}
/**======================================================
下面才是主要内容
=======================================================**/
// 是否订阅的标志
let isSubscribed = true
// 主要作用: 如果nextListeners等于currentListeners,则currentListeners复制的结果赋值给nextListeners
ensureCanMutateNextListeners()
// 将订阅函数添加到队列
nextListeners.push(listener)
// 取消订阅函数
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#subscribelistener for more details.'
)
}
// 将取消订阅标志设置为false
isSubscribed = false
// 主要作用: 如果nextListeners等于currentListeners,则currentListeners复制的结果赋值给nextListeners
ensureCanMutateNextListeners()
// 移除掉订阅函数
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
上面中的ensureCanMutateNextListeners()如下:
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
思考1:为什么需要
nextListeners,它有什么作用吗?只有currentListeners不行吗?
我们知道nextListeners和currentListeners都是用来保存订阅函数的,从函数ensureCanMutateNextListeners()中也可以,nextListeners的用处就是复制currentListeners。如果不复制行不行,或者只使用currentListeners会有什么问题吗?
其实:nextListeners的作用就是为了避免:在订阅器的回调函数执行时,用户继续订阅或者取消订阅,会引起bug,举个例子:
// 如果增加了回调函数A
const unsubscribe = store.subscribe(function A() {
// 将自己取消订阅
unsubscribe()
})
还记得dispatch() 函数中下面代码:
// 取出保存的所有订阅的函数,然后执行。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
在依次执行订阅器的回调函数时,比如A在队列中是第一个(即索引0),队列中还有B,C,D等回调。
如果A执行完以后,取消订阅,即从队列中将自己删除,那么此时listeners.length已经改变,B的索引为0。在下一轮循环,此时索引为1,就会导致B函数没有执行。
如果A执行完以后,又继续订阅,listeners.length也会改变,此时队列最后一个为新增的回调函数,那么在本轮循环它会立即触发,但是这不符合redux设计理念,新增的订阅函数应该在下一次dispatch()中触发。
思考2:接着思考1,有人可能又会说,那我们为什么将
dispatch()改为下面的形式:
const listeners = currentListeners.slice()
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
确实,改成这个形式,是可以解决这个问题,我最初也这么想的,直到我找到redux的代码提交记录:
可以看到最初redux代码也是类似这么写的,至于currentListeners和listeners谁复制谁?都不重要。
提交信息上表明大概的是:如果我们这么写,那岂不是每次dispatch都会复制一份队列吗?这会浪费资源,我们应该在需要的时候复制,即在订阅或者取消订阅时进行复制。
因此通过上面的这行代码const listeners = (currentListeners = nextListeners)可以保证,每次dispatch()时,currentListeners和nextListeners一定相同,则肯定会执行ensureCanMutateNextListeners()函数,来进行复制。
订阅或者取消订阅时复制还有另外一个好处,可以保证当前订阅或者取消订阅会在下一次dispatch()中生效。
最后,ensureCanMutateNextListeners()函数为什么还需要判断if (nextListeners === currentListeners),直接复制一份不行吗?这样也是为了避免同时多次添加订阅回调函数时,而产生的不必要的复制。
思考3:取消订阅函数为什么末尾要将
currentListeners = null?
这是因为我们此时取消订阅时,仅仅把nextListeners中的回调订阅函数进行删除,但是此时currentListeners中仍会保存被取消的回调函数,这会引起内存泄漏。此时的currentListeners又没有被其他地方用到,完全可以设置为null。
思考4:
subscribe()为啥不将state作为参数传入回调函数中,而每次都使用store.getState()获得?
// 为什么需要这么获得`state`
store.subscribe(() => console.log(store.getState()));
// 而不是直接将`state`作为参数传递
store.subscribe(state => console.log(state));
我也感觉这是很方便的,这个问题也是开发者给redux提的issue比较多的一个问题,我找了的issue,终于找到了作者的回答。
简要叙述下作者的意思是:
如果要是传入state作为参数,那么是不是也应该传入previousState,这样可以很方便开发者进行对比,而处理不同的逻辑?但是这要确保当store被Redux DevTools检测时,能够正常工作。作者认为每个使用getState()方法的store扩展,也不得不特别注意这个参数,感觉就像你为了让低级api对消费者更友好而付出的代价。
store的api是可扩展的,这就要求,每个函数的功能应该尽可能的单一化,原子化,而不应该重复。如果一个扩展想要在state传递给消费者之前做一些事情,如果我们传入state,那么我们不得不在两个地方处理,这很容易出错。
思考5:
subscribe()如果多次订阅同一个函数,会发生什么?
举个例子,有个公共函数,依次在组件A, B分别订阅了,此时的nextListeners会包含2个相同的函数,subscribe()函数里面并没有做任何校验,所以此时如果在B组件中取消了订阅,实际上是取消了组件A中的订阅的回调函数。
redux官方给出的解释是:这种情况很少见,认为并没有实际的使用场景。因此实际开发中我们要避免这种写法。
注:在subscribe(function() {store.dispatch()})会导致无限循环。
2.4 replaceReducer()
它可以用于取代当前的reducer()函数。这是一个高级的api,如果你想要的动态的加载reducer,可以使用它。
用法如下:
store.replaceReducer(newReducer)
源码如下:
function replaceReducer(nextReducer) {
// 如果不是函数则抛出错误。
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
// 将reducer重新赋值
currentReducer = nextReducer
// 触发一个action,以便于store.getState()能得到最新的值。
dispatch({ type: ActionTypes.REPLACE })
}
3 combineReducers()
随着应用变的越来越复杂,你可能会将
reducer()函数分割成多个reducer(),并且每个reducer()对应独立的state。一个reducer()并不能满足要求,此时combineReducers()应运而生。
- 它接收一个对象,该对象每一个key对应的value都为一个
reducer()函数。 - 它的返回值是合并好的
reducer()函数。通过这个返回值计算出的state同样是一个对象,key值与传入combineRedeucers()的key相同,value则为相应的state。
用法如下:
import {reducer1, reducer2, reducer3} = "./reducer"
const reducer = combineReducers({reducer1, reducer2, reducer3})
const store = createStore(reducer)
函数内部主要功能:
- 校验传入的参数是否为对象,每个key对应的reducer是否为函数,以及reducer是否设置默认值等。
- 返回整合好以后的reducer。
该函数源码主要内容其实没有多少,大部分都是判断用户传入的值是否正确。
- 源码如下:
function combineReducers(reducers) {
// 获取传入的key
const reducerKeys = Object.keys(reducers)
// 经过处理过的reducers
const finalReducers = {}
// 循环,分别判断传入的reducer类型为undefined,则警告,如果类型为function则加入到finalReducers中。
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') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 缓存初始化传入的state中有,但finalReducers里没有的key。
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
// 用于判断每个reducer是否设置默认值,以及reducer中是否使用了内置的actionType。
assertReducerShape(finalReducers)
} catch (e) { // 如果没有设置默认值或者使用redux内部的`actionType`则先缓存起来。
shapeAssertionError = e
}
/**======================================================
下面才是主要内容
=======================================================**/
// 最终的reducer函数
return function combination(state = {}, action) {
// 如果每个reducer没有设置默认值,或者使用了内置的actionType则为reducer函数,则抛出错误。
if (shapeAssertionError) {
throw shapeAssertionError
}
// 如果初始化传入的state中有,但finalReducers里没有的key。则抛出警告。
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] // 当前key
const reducer = finalReducers[key] // 当前reducer
const previousStateForKey = state[key] // 旧的state
const nextStateForKey = reducer(previousStateForKey, action) // 新的state
// 如果新的state为undefined, 抛出错误。
// 很多人可能好奇,之前不是已经校验reducer的返回值了,现在为啥还要校验。
// 因为之前校验的使用内部的actionType,但是有可能用户针对某一个actionType返回undefined。主要是为了避免出现bug。
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 开始hasChanged的值为false, nextStateForKey和previousStateForKey一旦不相等,则hasChanged 就为true,在下一轮循环就不需要继续对比它俩的值。
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
}
由上述代码可知,判断hasChanged和为true的条件是
finalReducers中任意一个reducers返回值和上次不同,则为true。finalReducerKeys的长度和初始化传入的state中的key的长度不同,则为true。
思考1
shapeAssertionError为什么不直接抛出,而是在返回的函数里面抛出。
如果直接抛出的话,会导致整个js线程执行中止,creatStore()也会失败,随之react应用也会构建失败。作者认为在这里抛出错误,并不合适。而是在每次调用dispatch()时,就直接抛出一次,因此combination()里报错是最友好的。
思考2: 为什么判断
finalReducerKeys.length !== Object.keys(state).length这个?
虽然之前有过检验初始化的key(即缓存在unexpectedKeyCache变量中),但只是抛出警告,并没有抛出错误,如果有unexpectedKeyCache(也就是上面等式不成立),则也会认为hasChanged为true。(这个等式不成立的时刻也只有在第一次dispatch()时,因为后续的state都是根据finalReducers计算出来的,所以二者不可能不等。)
- 下面是
assertReducerShape()函数的代码:
function assertReducerShape(reducers) {
// 遍历finalReducers,检测每个reducer结果是否为undefined,如果为undefined则报错
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
// 使用内部的type:ActionTypes.INIT测试reducer的返回值
const initialState = reducer(undefined, { type: ActionTypes.INIT })
// 如果结果为undefined,说明该reducer没有返回默认值。
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.PROBE_UNKNOWN_ACTION()会返回一个随机的type
// 进入到这一步,有两种情况:
// 1,当前reducer里面对type为ActionTypes.INIT做了判断,并返回非undefined的值。
// 2, 当前reducer里面没有对type为ActionTypes.INIT做判断,即:当reducer不识别某个actionType时,也会返回非undefined的值(这正是我们期望的)
// 如果下面的:传入随机的type,reducer返回undefined,可以排除上面第2种情况,说明代码中使用仅供redux内部使用的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.`
)
}
})
}
思考1:假如,我们在
createStore()传入了初始值,但是单独的reducer()函数没有设置初始值,会发生什么?
举个例子:
import {reducer1, reducer2, reducer3} = "./reducer"
const reducer = combineReducers({reducer1, reducer2, reducer3})
const initState = {reducer1: 1, reducer2: 2, reducer3: 3}
// 传入初始值
const store = createStore(reducer, initState)
reducer1函数类似下面的形式,即函数中没有设置默认值:
reducer1(state, action) {
return state
}
在上面的assertReducerShape()函数中有一行代码:const initialState = reducer(undefined, { type: ActionTypes.INIT }),这个代码中由于传入undefined,并没有传入createStore()初始值,所以会报错。
因此,设置初始值,我们最好是在每个独立的reducer()函数中。
思考2:接着思考1,
assertReducerShape()的每个独立的reducer()为什么通过传入undefined来进行判断?
因为传入undefined,因为reducer()函数返回undefined是没有任何意义的,通过传入undefined能够更好的判断用户写的reducer()函数的健壮性。
思考3:每次
dispatch()以后,所有的reducer()都会执行,这会影响性能吗?
官方认为,虽然会影响,但可以忽略不计,因为js引擎每秒可以运行大量的函数,并且reducer函数还是纯函数,全有能力hold住。如果你确实担心这方面的性能问题,可以使用插件。
4 bindActionCreators()
bindActionCreators()是将actionCreator()和dispatch()函数进行绑定。
它接收2个参数,第一个参数为函数/对象,第二个参数为dispatch。
-
如果第一个参数为函数(某个
actionCreator()),则返回一个函数(actionCreator()与dispatch()整合函数) -
如果第一个参数为对象(多个
actionCreator()),则返回一个包含相同key的对象。其一般用于react-redux中的mapDispatchToProps上面。
用法如下:
const actionCreators = {
addAction(value) {
return {type: "ADD", value}
},
delAction(value) {
return { type: 'DEL', value }
}
}
const boundActions = bindActionCreators(action, dispatch)
boundActions.addAction(2); // 相当于dispatch(actionCreators.addAction(2))
boundActions.delAction(1); // 相当于dispatch(actionCreators.delAction(1))
函数内部主要功能:
- 将
dispatch()方法和actionCreator()进行绑定。
注:actionCreator(),我们已经知道action是一个对象,形如{type: "ADD", value: 2},actionCreator()顾名思义就是生成action的函数,如下:
// getAddAction就是一个actionCreator()函数
const getAddAction = (value) => {type: "ADD", value}
源码如下:
function bindActionCreators(actionCreators, dispatch) {
// 如果为函数,则直接调用bindActionCreator函数
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 如果actionCreators不是对象,并且不为null,则直接抛出错误
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"?`
)
}
// 是对象,则循环,依次调用bindActionCreator函数
const boundActionCreators = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
bindActionCreator()源码如下:
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
5 compose()
compose函数的主要功能就是:将多个函数,如a,b,c作为参数传入(compose(a, b, c)) 并转为(...args) => a(b(c(args)))形式。一般用于组合多个中间件函数。
源码如下:
export default function compose(...funcs) {
// 没有参数,则返回一个默认的函数arg => arg
if (funcs.length === 0) {
return arg => arg
}
// 如果只有一个参数,那就将其返回
if (funcs.length === 1) {
return funcs[0]
}
// 转化
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
6 applyMiddleware()
applyMiddleware()接收多个中间件函数,用法如下:
const store = createStore(reducer, applyMiddleware(middlewareFn))
6.1 中间件函数的由来
- 如果在每次一状态改变以后,都会将
action和新的state记录,通常我们会像下面这么做:
const action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
- 上面的方式可以实现,但是我们不想每次都写那么多代码,这是可能会做如下封装:
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
// 调用
dispatchAndLog(store, addTodo('Use Redux'))
- 此时,虽然会解决问题,但是每次都会引入函数(
dispatchAndLog())并调用dispatchAndLog(store, addTodo('Use Redux')),这时我们可能会继续做如下封装:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
通过将dispatch重新赋值,我们可以一劳永逸。但是问题又出现了,假如我要应用多次这样的变换呢?比如:我每次dispatch(),我还需要记录错误日志呢,这会应该怎么办?
- 如果我们要实现一个上报错误的中间件呢?(有人可能会说,记录错误,可以使用
window.onerror,但是它并不是可靠的,因为在一些较旧的浏览器中,它不提供堆栈信息(这对于理解为什么会发生错误至关重要)。)如果调用dispatch()方法以后,发生任何报错,都会上报,这岂不很好吗?
我们知道,保持记录和上报错误这二者的分离,也是很重要的,他们应该属于不同的模块,因此我们可能会做如下封装:
function patchStoreToAddLogging(store) {
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function patchStoreToAddCrashReporting(store) {
const next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
// 进行错误上报,例如:
postData("/error", {error, action, state: store.getState()})
throw err
}
}
}
// 调用
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
- 但是上面的仍然不是很好,侵入性比较强,属于MONKEY PATCHING(即:猴子补丁,说的是在函数或对象已经定义之后,再去改变它们的行为。))希望的是每一个功能函数,尽可能不会修改外面的变量或者函数,从而产生一些副作用。如果我们将这个函数返回,交由外面去处理。例如:如果我们将上面的函数变成如下这种形式,岂不很好:
function logger(store) {
const next = store.dispatch
// 之前:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
并且同时在redux内部也可以一个“助手”,来帮助我们处理这些:
function applyMiddlewareByMonkeypatching(store, middlewares) {
// 这一步的目的是, 在之前的代码,我们应用了两个中间件,随后当我们dispatch一个action时, 最后的中间件先执行,这可能明显不符合大部分人的习惯。所以需要转换
middlewares.reverse()
middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}
// 此时我们就可以使用:
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
- 但是上面的logger函数,仍然属于MONKEY PATCHING(即:猴子补丁,说的是在函数或对象已经定义之后,再去改变它们的行为。)。 其实logger还有另外一种方式实现,如果我们不用
next = store.dispatch而是作为参数实现呢?
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}
那么此时,我们将applyMiddlewareByMonkeypatching改名为applyMiddleware,并将它变为下面这个:
function applyMiddleware(store, middlewares) {
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
return Object.assign({}, store, { dispatch })
}
最终这个applyMiddleware函数和源码中的很像了,但是还有三方面不同:
- 源码中并没有暴露
store整个api,而是将dispatch()和getState()方法。 - 源码中做了一个判断,如果在构建中间件的时候,调用
dispatch()方法则会报错。 - 为了确保中间件只能应用一次,所以应用中间件的时机是在
createStore()执行时。
源码如下:
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
// 设置初始值
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)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
还记得createStore(reducer, preloadedState, enhancer)函数里的enhancer(createStore)(reducer, preloadedState)这行代码吗?而这个enhancer参数通常就是applyMiddleware()。
思考1: 为什么源码中这么写
dispatch: (...args) => dispatch(...args)而不是dispatch: dispatch?
从源码中看到中间件的函数主要作用就是返回一个新的dispatch,如果直接dispatch: dispatch这么写,那么此时的middlewareAPI.dispatch就永远是下面这个函数:
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
栗子:
let fn = () => console.log("a")
let obj = {fn}
fn = () => console.log("b")
console.log(obj.fn) // 还是为() => console.log("a")
思考2:那么问题又来了,接着思考1,为什么要给dispatch的初始值设置为抛出错误的函数?
就是为了防止在中间件构建时(即中间件内部第一层,第二层)立即调用dispatch()方法。
例如下面一个中间件:
({ dispatch, getState }) => {
如果此时立即调用dispatch()则会报错。
dispatch()
return next => action
}
思考3:我们自己的
applyMiddleware赋值了三次dispatch即middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))而,实际的源码却只赋值了一次,这是怎么做到的?
就是通过compose()方法实现的,它可以将形如(compose(a, b, c)) 并转为(...args) => a(b(c(args)))形式。这个返回结果再传入dispatch就可以将dispatch()一层一层包裹起来。
如:(...args) => a(b(c(args)))此时将dispatch传入以后。
- c(args)中的args(即此时的
next())就是原始的dispatch(),返回值就是c中间件返回的新dispatch()。 - 同理:对于函数b的参数(此时的
next())就是c的dispatch()。 - 对于函数a的参数(此时的
next())就是b的dispatch()
因此最终当我们调用store.dispatch()方法时,最终各个中间件的执行顺序就是:
- a的
dispatch()开始执行。 - b的
dispatch()开始执行。 - c的
dipatch()开始执行。 - c的
dispatch()执行结束。 - b的
dispatch()执行结束。 - a的
dispatch()执行结束。
执行顺序就是洋葱模型:
因此在中间件第三层调用dispatch以后会将所有的中间件都执行一遍。而调用next则会调用下一个中间件。
7 redux-thunk源码
thunk中间件函数就不难理解了,只不过多了传入参数的功能extraArgument。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
// 如果要是个函数就将dispatch传入。
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
使用方法
const store = createStore(reducer, applyMiddleware(thunk))
const asyncAction = async (dispatch) => {
let res = await postData('/a')
dispatch(action)
}
// 此时可以派发一个函数
store.dispatch(asyncAction)