redux 闲谈
起因: 在与涂鸦智能一个 web 工程师交流过程中,他询问我 dispatch 一个 action,是如何和 reducer 绑定的,dispatch(actionA)只会触发 reducerA 却不会去触发 reducerB.
Github https://github.com/reduxjs/redux
redux 数据流程
redux 遵循严格的单向数据流,以 React 为例如下图:
(网图,侵删)
- 通过用户在 ViewUI 进行一个 dispatch(action);
- Store 内部自动通过如下形式 Reducer(prevState, action)调用
- Reducer 返回新的 State(newState), state 变化后调用 Store 上的监听器(store.subscribe(listener))
- 在 listener 内部可以通过
store.getState()
方式得到最新的 state 进行数据操作
初始化
redux 的 Store
初始化通过 createStore
方法来进行初始化
const store = createStore(combineReducers, prevState, compose(applyMiddleware(...middleware)))
-
combineReducers
合并后的 reducer,reducer 形式如下
function authReducer(state, action) {switch(action.type) {
case 'login':
return {...state, isLogin: true}
default:
return {...state}
}
}
function userReducer(state, action) {// ... 如上}
通过使用 combineReducers({authReducer, userReducer}) 返回一个 reducers
-
prevState
则为 reducer 中 state 的初始化默认值,这里默认值为整个状态树的默认值 -
middleware
则为 redux 中间件, 增强 redux 功能
该部分初始化流程阶段,在下面 applyMiddleware 会再次调用
combineReducers 合并 reducer
在上面说到我们可能存在多个 reducer,也可能分模块来处理不同的状态问题,这里就需要合并不同模块的 reducer,实现代码:
export default function combineReducers(reducers) {const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {const key = reducerKeys[i]
// 其他代码...
if (typeof reducers[key] === 'function') {finalReducers[key] = reducers[key] // ①
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 其他代码...
return function combination(state = {}, action) { // ②
// 其他代码...
let hasChanged = false
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') {const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
传入一个 reducers = {authReducer, userReducer}, 很明显对 reducers 进行了对象遍历,在①这个位置进行了单个 reducer 函数的拷贝,在②这个位置 redux 内部自己创建了一个 reducer 函数为combination
, 在是③这个位置,进行了开发者定义的 reducer 定义,也就是说 dispatch(action) -> combination(state, action) -> customReducer(state, action), 在循环内部每次获取对应的 module 的 state 值作为 previousStateForKey
, 传入用户的 reducer 中,所以用户 dispatch 的 action 中 type 是和 reducer 对应的位置在于用户自己的判断
compose 和 applyMiddleware
compose
compose 函数非常简短, 代码如下:
export default function compose(...funcs) {if (funcs.length === 0) {return arg => arg}
if (funcs.length === 1) {return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
传入一个不定量函数作为参数,主要作为函数从一个数组形式例如 [login, fetchToken, fetchUser] 这样的函数传入,得到则是 fetchUser(fetchToken(login(…args))) 这样的形式, 将函数组合起来,并从右到左,而且最右侧函数可以接受多个参数
示例仅仅为了说明,不在实际业务中出现
applyMiddleware
根据函数字面意思该函数为应用 redux 中间件,核心代码如下:
export default function applyMiddleware(...middlewares) {return createStore => (...args) => { // 这里 createStore 通过初始化时候传入
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
}
}
}
示例 logger
const logger = store => next => action => { // example-①
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
// usage: applyMiddleware(logger)
这里 applyMiddleware 接受不定量的 redux 中间件,我们就来解释一下 example-①
这里申明是哪里来的。
在 applyMiddleware
源码中,传入 middlewares 后,在 ① 的位置就行了第一个次的中间件调用传入 middlewareAPI,分别为 getState
和 dispatch
两个方法这里对应 example-①
中的 store, 在 ② 的位置进行了上文说的compose
调用, 把所有的中间件进行了组合,从右到左的调用,此刻传入 dispatch 方法,这里方法对应 example-①
中的 next
,在上文中说到 compose
对 函数进行了组合,我们这里将 store.dispatch 传入当成参数,返回一个新的函数等价于我们在 ViewUI 中 dispatch 的时候其实使用的 compose(...chain)(store.dispatch)(action)
这样的方式,所以在 example-①
中 action 是开发者的 action。使用中间件后的 store.dispatch 也是通过中间件包装后的 dispatch。在最后 applyMiddleware
把 dispatch 返回。
这里有点晦涩难懂在于 compose(…chain)(store.dispatch)(action), 这里可以这样理解,每次 dispatch 的时候,中间件都是会执行一次,传入顺序是[logger, crashReport], 执行顺序为 crashReport -> logger, 自右向左执行。在每个中间件执行过程中都需要返回 next(action) 将当前 action 继续传递下去
其他参考: redux applyMiddleware 解析
dispatch(action)
下面说到 dispatch,这是我们经常用的,如下代码:
function dispatch(action) {
// 其他代码
try {
isDispatching = true
currentState = currentReducer(currentState, action) // ①
} finally {isDispatching = false}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {const listener = listeners[i]
listener() // ②}
return action
}
这里先说一下 isDispatching
作用,isDispatching 执行 reducer 判断
isDispatching
really needed?
这里这个参数解决情况如下:
var store = createStore((state={}, action) => {if (something) {store.dispatch({type: 'ANOTHER_ACTION'})
}
return state
})
继续下面来说在 ① 的位置执行 currentReducer, 这里 reducer 为我们通过 createStore
传入 combineReducers, 把对应的 currentState 和 action 传入,currentState 也是在初始阶段传入的 preloadState。在 ② 的位置则进行触发监听器,监听器设置则在 store.subscribe
中增加。
现在来解释,action 和 reducer 对应关系
我当时回答的是:action.type 对应 reducer 的函数名称,如果一致的话就会执行。呵呵,那个涂鸦大工程师有问题了如何绑定的呢,怎么确定是 reducerA 而不是 B。呵呵呵呵呵。NB
后续
还问我 redux-saga、redux-thunk 异步处理方案,我提了一下 redux-thunk,后续继续更新。
PS:真应该提升整个沟通流程质量
1. 参考地址: https://redux.js.org