redux 作为 React 全家桶成员经常在项目中使用,在刚接触 redux 时自己总是有些困惑 ? :
- reducer 是什么?
- 为什么需要 combineReducers?
- middlewares 的机制是什么?
通过翻阅 redux 官网,慢慢疑惑被解开
先上一张自己对 redux 整体流程的理解:
reducer 与可选的 preloadState、applyMiddleware 传入 createStore 创建出拥有 dispatch、getState、subscribe 方法的 store 对象
首先从 reducer 讲起,reducer 其实是更新 state 的具体逻辑
:
function reducer(prevState, action) {
const { type, payload } = action
switch(type) {
// ...
default:
return prevState
}
}
复制代码
而 dispatch 通过调用 reducer 来更新 state:
store.dispatch = (action) => {
store.state = reducer(store.state, action)
}
复制代码
所以触发 dispatch 后的流程是 dispatch(action) => reducer(prevState, action) => newState
:
当 state 数据结构越来越复杂时,reducer 会包含着不同维度数据的更新逻辑,如:
let state = {
user: { id: 1, name: 'Daniel Yang', age: 26, avatar: '' },
role: { employer: true, developer: true, manager: false },
list: []
}
function reducer(prevState = state, action) {
const { type, payload } = action
switch (type) {
case 'UPDATE_USER':
return { ...prevState, user: payload }
case 'UPDATE_ROLE':
return { ...prevState, role: payload }
case 'REMOVE_ROLE':
return { ...prevState, role: prevState.role.filter(r => r !== payload.id) }
case 'ADD_LIST':
return { ...prevState, list: prevState.list.concat(payload) }
case 'REMOVE_LIST':
return { ...prevState, list: list.filter(l => l !== payload.id) }
default:
return prevState
}
}
复制代码
如果我们能把不同维度的数据拆成独立的 reducer,再由某个方法合并起来传进 createStore 创建 store,整体逻辑会清晰很多
于是 combineReducers
应运而生 ? :
function combineReducers(reducerObjs) {
return (prevState, action) => {
const combineState = {}
for (const key of Object.keys(reducerObjs)) {
// 获取子 reducer 方法和对应的数据
const reducer = reducerObjs[key]
const state = prevState[key]
combineState[key] = reducer(state, action)
}
return {
...prevState,
...combineState
}
}
}
复制代码
现在可以这样拆分我们的 reducer:
function userReducer(prevState, action) {
const { type, payload } = action
switch(type) {
case 'UPDATE_USER':
return payload
default:
return prevState
}
}
function roleReducer(prevState, action) {
const { type, payload } = action
switch(type) {
case 'UPDATE_ROLE':
return payload
case 'REMOVE_ROLE':
return { ...prevState, role: prevState.filter(r => r !== payload.id) }
default:
return prevState
}
}
function listReducer(prevState, action) {
const { type, payload } = action
switch(type) {
case 'ADD_LIST':
return { ...prevState, list: prevState.concat(payload) }
case 'REMOVE_LIST':
return { ...prevState, list: prevState.filter(l => l !== payload.id) }
default:
return prevState
}
}
const reducer = combineReducers({
user: userReducer,
role: roleReducer,
list: listReducer
})
复制代码
随着使用的深入,日志记录、错误捕获、异步数据流等都是常见需求
以日志记录为例,如果我们想记录每次发出的 action 和更新后的 state,在现有基础上该如何实现呢?
尝试一:我们可以在每个调用 dispatch 的地方前后加上打印:
console.info('action: ', action)
store.dispatch(action)
console.info('state: ', store.getState())
复制代码
但显然并不适合,每发起一个 dispatch,我们都得在前后手动写上打印方法,方法太原始
尝试二:要不我们重写下 store.dispatch 方法吧 ? :
const next = store.dispatch
store.dispatch = action => {
console.info('action: ', action)
next(action)
console.info('state: ', store.getState())
}
复制代码
现在我们调用 dispatch 方法时都会在前后打印 action 和 state,看起来很完美 : )
但如果我们有多个扩展想应用在 dispatch 期间呢 ? ,比如增加错误捕获:
const next = store.dispatch
store.dispatch = action => {
try {
next(action)
} catch (err) {
console.error(err)
}
}
复制代码
尝试三:于是很自然地想到封装 store.dispatch 方法 ? :
function addLog(store) {
let next = store.dispatch
store.dispatch = action => {
console.info('action: ', action)
next(action)
console.info('state: ', store.getState())
}
}
function handleError(store) {
let next = store.dispatch
store.dispatch = action => {
try {
next(action)
} catch (err) {
console.error(err)
}
}
}
addLog(store)
handleError(store)
复制代码
现在用是可以用了,但再想想能否将中间件优化成链式调用,而不是每次使用都需增加一行声明呢?
尝试四:可以返回扩展后的 dispatch ? ,这样可以让多个中间件串联起来,得到最终的 dispatch:
// 中间件示例
function middleware(store) {
let next = store.dispatch
return action => {
// ... do something
return next(action)
}
}
function applyMiddleware(store, middlewares) {
middlewares.forEach(middleware => {
store.dispatch = middleware(store)
})
}
复制代码
尝试五:如果我们将 dispatch 作为参数传递进去,而不是每次在函数内部通过 store.dispatch 获取岂不是更好 ? :
// 中间件示例
function middleware(store) {
return next => {
return action => {
// do something
return next(action)
}
}
}
// 相应地 applyMiddleware 做了调整
function applyMiddleware(store, middlewares) {
// 调整中间件顺序,在包裹后执行时正是中间件传进去的顺序
middlewares = middlewares.reverse()
let dispatch = store.dispatch
// 前一个 middleware 返回的 dispatch 会作为下一个 middleware 的 next
middlewares.forEach(middleware => {
dispatch = middleware(store)(dispatch)
})
return { ...store, dispatch }
}
复制代码
这与 redux 源码中 applyMiddleware 的实现很接近了 ? ,相比之下,源码的实现有几个更完善的地方:
- 顶层的 store 参数只暴露必需的方法,而不是整个 store 对象;
- 如果在 middleware 中直接调用 store.dispatch 而不是 next(action),那么会再次遍历整个中间件链,这对异步的中间件非常有用;
- 为了保证只应用中间件一次,它会作用在 createStore() 上而不是 store 本身。
源码的大致实现:
重点介绍 applyMiddleware 部分
function createStore(reducer, preloadState, applyMiddleware) {
return applyMiddleware(createStore)(reducer, preloadState)
}
function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
// 创建 store
const store = createStore(...args)
// 只传递必要的方法给 middleware
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 向将组合后的中间件链传入 store.dispatch,并重新赋值 dispatch
// 如果中间件里直接调用 store.dispatch,会重新走 compose(...chain)(store.dispatch) 方法
dispatch = compose(...chain)(store.dispatch)
// 返回新的 store 对象
return {
...store,
dispatch
}
}
}
function compose(...funcs) {
// 组合中间件链:(e, f, g) => (...args) => e(f(g(...args)))
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码
最后看下如何通过中间件实现异步数据流,以 redux-thunk 为例:
function reduxThunkMiddleware(store) {
return function (dispatch) {
return function (action) {
// 如果发出的 action 是一个方法,将约定好的参数传给 action 执行
// 可以在 action 方法内部决定何时发出 dispatch
if (typeof action === 'function') {
return action(dispatch, store.getState)
}
return dispatch(action)
}
}
}
复制代码
现在对于开头提到的问题,你有自己的答案了吗 ? :
- reducer 是什么?
- 为什么需要 combineReducers?
- middlewares 的机制是什么?