Redux-中间件原理总结
演化流程
参考:官网
我们都知道使用 createStore 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。
但是,如果你用 applyMiddleware(中间件) 来套住 createStore 时,middleware 可以修改 action 的执行,并支持执行 dispatch intent(意图)。Intent 一般是异步操作如 Promise、Observable 或者 Thunk。
像 redux-thunk 或 redux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch一些除了 action 以外的其他内容,例如:函数或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。比如,支持 Promise 的 middleware 能够拦截 Promise,然后为每个 Promise 异步地 dispatch 一对 begin/end actions。
当 middleware 链中的最后一个 middleware 开始 dispatch action 时,这个 action 必须是一个普通对象。这是同步式的 Redux 数据流开始的地方
这也是我们异步Action的主要方式。
工具函数Action Creator
function addTodo(text) {
return {
type: ADD_TODO,
text,
}
}
需求:
- 在action发起和每次state被计算完成时都将它们记录下来,添加日志功能
- 在action中添加崩溃报告(生产过程中报告 JavaScript 的错误)
尝试1:
手动记录
// # 1. 手动记录
let action = addTodo('use Redux')
console.log('dispatching 分配开始', action)
store.dispatch(action)
console.log('开始记录改变值 ', store.getState())
尝试2:
封装为函数,每次都要将其导入
// # 2. 封装为函数
function dispatchAndLog(store, action) {
console.log('dispatching 分配开始', action)
store.dispatch(action)
console.log('开始记录改变值 ', store.getState())
}
// 然后用它替换 store.dispatch()
dispatchAndLog(store, addTodo('Use Redux'))
尝试3(Monkeypatching ):
替换dispatch,用新包装的dispatch替换store的dispatch
Monkeypatching :在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。这里就是在有中间件加入的时候,动态替换store的dispatch方法。
// # 3. 替换dispatch
// 获取之前的dipatch
let next = store.dispatch
// 替换dispatch
store.dispatch = function dispatchAndLog(store, action) {
console.log('dispatching 分配开始', action)
// dipatch() 返回值为:要dispatch的action; 及参数action。方便继续分发,后续中间件可以继续使用
let resulte = next(action)
console.log('开始记录改变值 ', store.getState())
return resulte
}
尝试4:
如果有多个中间件;如上面提及的需求2,且各中间件为独立模块,就需要封装尝试3的替换方式
// # 4. 不同功能封装替换store.dispatch方法
//记录日志 封装替换dispatch
function patchStoreToAddLogging(store) {
let next = store.dispatch
// 替换dispatch方法
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
// 记录崩溃 封装替换dispatch
function patchStoreToAddCrashReporting(store) {
let next = store.dispatch
// 替换dispatch方法
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('捕获一个异常!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState(),
},
})
throw err
}
}
}
// 不同中间件依次替换store.dispatch方法,新的dispatch,就包含有2个中间的功能
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
尝试5:
将替换dispatch抽离出来,做为一个方法,隐藏替换dispatch(隐藏Monkeypatching )
// # 5. 利用applyMiddlewareByMonkeypatching 隐藏替换dispatch过程
function logger(store) {
let next = store.dispatch // 步骤1:获取该中间件前面的 dispatch方法
// 我们之前的做法: 直接替换dispatch
// store.dispatch = function dispatchAndLog(action) {}
// 直接返回新dipatch方法,然后后续中间件直接通过store.dispatch获取; 如步骤1
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function applyMiddlewareByMonkeypatching(store, middlewares) {
// 为数组
middlewares = middlewares.slice()
// 反序数组 包装由里向外 运行由外向里
middlewares.reverse()
middlewares.forEach((middleware) => {
// 依次包装dispatch 包装由里向外 运行由外向里
store.dispatch = middleware(store)
})
}
// 运用
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
尝试6:
尝试避免:每次封装中间件时,都需要替换dispatch,然后封装下个中间件,又需要通过store.dispatch获取。
改进思路:通过每次封装中间件时,接受一个next函数(上个中间件返回的dispatch),返回一个dispatch,作为下个中间件的的参数。最后在同一替换dispatch。
好处:
相比尝试5最大的好处,在调用中间的next函数,是调用上一个中间件返回的dispatch。如果直接调用store.dispatch,那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。造成死循环。
这也是为什么在中间件调用千万别在某个中间件中直接调用store.dispatch, 不然会无限循环的原因。
// # 6. 移除替换dipatch功能 提取之前的dipatch作为参数传入下一个中间件
// 日志功能中间件
function logger(store) {
// 传入next()的dipatch函数, 并返回一个dispatch函数,并作为下个中间件的next函数
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}
// 柯里化函数
// 日志功能
const logger = (store) => (next) => (action) => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
// 捕获异常功能
const crashReporter = (store) => (next) => (action) => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState(),
},
})
throw err
}
}
// 包装中间件,并取得最终的dipatch方法 并返回一个 store 的副本
function applyMiddleware(store, middlewares) {
// 为数组
middlewares = middlewares.slice()
// 反序数组 包装由里向外 运行由外向里
middlewares.reverse()
//相对于尝试5,代码的改进
let next = store.dispatch
middlewares.forEach((middleware) => {
next = middleware(store)(next)
})
return Object.assign({}, store, { dispatch })
}
推到结束!
最终方法(redux源生API的方式):
不同于尝试6:为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (…middlewares) => (createStore) => createStore
由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
// 然后是将它们引用到 Redux store 中
import { createStore, combineReducers, applyMiddleware } from 'redux'
let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
// applyMiddleware() 告诉 createStore() 如何处理中间件
applyMiddleware(logger, crashReporter)
)
// 就是这样!现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:
// 将经过 logger 和 crashReporter 两个 middleware!
store.dispatch(addTodo('Use Redux'))
源码解析
…后续补充
redux-thunk源码分析
参考:React 之 Redux 异步处理——redux-thunk
…后续补充
redux-saga源码分析
…后续补充