在复杂前端应用中,想要优雅的实现各个子模块之间的通信管理是较为困难的,因为每个模块间进行独立通信时会存在时序难控制、状态难溯源等问题。比如多个模块都需要修改模块 A 中的数据,怎么控制他们的修改顺序、怎么记录修改的过程? Redux 就是解决了上述的问题,通过提供一个全局唯一的状态仓库,以及单向数据流操作。使得状态可管理、可溯源、可控制时序。
Redux 是基于 Facebook 提出的 Flux 设计模式进行实现,Flux 的核心理念是单向数据流:
View->Actions->Dispacher->Store->View
View 层触发 Actions 事件,交给 Dispacher(本质上是注册到 Store 上的回调) 统一处理后,再根据 Action 的类型,执行对应修改 Store 数据的操作。Store 数据修改完成后,会派发一个广播事件给 View 层方法,这些方法可以获取 Store 的最新 State 并视情况更新 View。
这种设计的好处是其数据是单一方向流动的,数据的修改一定会经过 Actions、Dispacher 动作,这使得对数据的修改变得可预测、可溯源。
基于此,Redux 中数据更新的设计架构如下:
与上图中 Redux 的设计架构相对应,在源码中
- createStore 负责实现 Store 核心功能,创建 State、串联触发 action 到更新 state 的整体逻辑。
- applyMiddleware、compose 实现 middleware 相关逻辑
- combineReducers 实现组合多个 reducer
redux/src
.
├── applyMiddleware.ts // 实现 applyMiddleware 方法
├── bindActionCreators.ts // 实现 bindActionCreators 方法
├── combineReducers.ts // 实现 combineReducers 方法
├── compose.ts // compose 方法,内部供 applyMiddleware 使用,同 lodash 的 flowRight
├── createStore.ts// 实现 createStore 方法
├── index.ts
├── types
│ ├── actions.ts
│ ├── middleware.ts
│ ├── reducers.ts
│ └── store.ts
└── utils // 一些辅助函数,可不用关心
├── actionTypes.ts
├── formatProdErrorMessage.ts
├── isAction.ts
├── isPlainObject.ts
├── kindOf.ts
├── symbol-observable.ts
└── warning.ts
基于上文对 Flux 设计模式的描述可以得出,要实现 Flux 模式的第一步是创建 Store,因为 Dispatcher 和 View 都依赖于 Store 进行开展。对应在 Redux 中,第一步便是使用 CreateStore 方法创建 Store。
CreateStore 用法如下:
const store = createStore(reducer, preloadedState, enhancer);
const {getState,dispatch,subscribe,replaceReducer} = store
createStore 函数接受三个参数 reducer, preloadedState, enhancer,返回一个 store 对象,其包含 getState,dispatch,subscribe,replaceReducer 四个方法。
首先来看对入参的处理,reducer 对应上文 Flux 设计模式中的 dispacher 逻辑,传入 createStore 后会先做暂存处理,preloadedState 则是作为初始化的 Store State 数据,重点在 enhancer。enhancer 官方解释为可以用于实现第三方功能,如 middleware、时间旅行、持久化等功能来增强 store。听起来比较复杂,但其实现方式却是很简单:
function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer // 记录当前的 reducer
let currentState = preloadedState // 记录当前的 state
if (typeof enhancer !== 'undefined') {
// 调用 enhancer
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState | undefined
)
}
// 省略其他逻辑
}
可以发现 enhancer 的实现非常巧妙,它允许用户“劫持” createStore 原生返回的 store 对象,修改其中的 getState,dispatch 等方法后,再返回对应的新方法。
一个 logEnhancer 的案例如下,使用后会在每次 dispach 时 log 相关信息。从这个例子我们可以发现,enhancer 的编写是用到了面向切面编程(AOP)的设计:在不改动目标函数原有功能的前提下,注入实现新的功能。
const logEnhancer = (createStore)=>{
return (reducer, preloadedState)=>{
const {dispach,...rest} = createStore(reducer, preloadedState)
return {
dispach:(action)=>{
console.log('当前 dispatch 的数据',actions)
dispach(action);
},
...rest
}
}
}
接下来分析 createStore 返回的 store 对象中几个方法的实现:getState,dispatch,subscribe,replaceReducer。
getState 的逻辑较为简单,即返回当前的 state 状态。
function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer // 记录当前的 reducer
let currentState = preloadedState // 记录当前的 state
let isDispatching = false // 是否正在进行 dispatch
function getState() {
return currentState // 通过 getState 获取当前的 state
}
// 触发 action
function dispatch(action: A) {}
function subscribe(listener: () => void) {}
function replaceReducer(nextReducer: Reducer<S, A>) {}
// 初次调用 createStore 时初始化 state
dispatch({ type: ActionTypes.INIT } as A)
// 返回一个 sttore
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer
}
return store
}
dispach 是 Redux 中实现修改数据的唯一途径,调用时需要传入一个 action 对象,随后 Redux 会自动调用 reducer 获取更新后的 state,再调用所有注册的 lisener 实现数据更新广播。
从代码中我们可以发现,dispatch 仅支持普通对象,如果需要支持 dispatch Promise 等其他类型的 action,就需要使用额外 reducer 对原始 dispach 函数进行封装(如:redux-thunk 等),以支持其他类型的 action 对象。
function dispatch(action: A) {
// 省略其他逻辑
// 在 Dispatching 过程中,不能再触发新的 dispatch
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 加上执行🔒
isDispatching = true
// 调用 Reducer 获取新的 state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
// 触发所有订阅事件
listeners.forEach(listener => {
listener()
})
return action
}
在 Redux 中,需要通过 subscribe 注册回调函数,在 Store 内的 state 发生变化后, Redux 将会调用所有注册的回调函数。同时 subscribe 也会返回一个 unsubscribe 函数,执行后可以删除对应已注册的回调函数。
function subscribe(listener: () => void) {
if (isDispatching) {
throw new Error()
}
let isSubscribed = true
ensureCanMutateNextListeners() // 给 nextListeners 开辟一个新的内存地址,确保对其的修改不影响当前正在执行的中的订阅函数队列
const listenerId = listenerIdCounter++
nextListeners.set(listenerId, listener) //nextListeners 添加订阅事件
// 取消订阅事件
return function unsubscribe() {
if (!isSubscribed) { // 防止调用多次 unsubscribe
return
}
if (isDispatching) {
throw new Error()
}
isSubscribed = false
ensureCanMutateNextListeners();
nextListeners.delete(listenerId)
currentListeners = null
}
}
ensureCanMutateNextListeners 的作用:
可以发现,在订阅以及取消订阅的过程中,都会先执行 ensureCanMutateNextListeners 函数,这个函数的作用是什么?
从代码中可以发现,该函数会在 nextListeners 和 currentListeners 指向同一处内存地址时,给 nextListeners 开辟一处新的内存地址,并将 currentListeners 中的回调函数地址同步到新的 nextListeners 中。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = new Map()
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener)
})
}
}
进一步分析,消费 nextListeners 和 currentListeners 的地方在哪里,答案在 dispach 方法中:
function dispatch(action: A) {
// 省略其他逻辑
// 这里会同步 nextListeners 的内存地址到 currentListeners,最终赋值给 listeners 后进行依次调用
const listeners = (currentListeners = nextListeners)
// 触发所有订阅事件
listeners.forEach(listener => {
listener()
})
return action
}
到这里即可明确 ensureCanMutateNextListeners 函数的作用:切割 listeners、currentListeners 和 nextListeners 的内存地址,保证在单次 dispatch 过程中 listeners 对应回调函数的数量不会发生变化,保证执行稳定性。
执行 subscribe/unsubscribe 之前:
listeners=currentListeners=nextListeners
开始执行 subscribe/unsubscribe :
listeners=currentListeners≠nextListeners
随后再执行修改 nextListeners 中的回调函数数量就不会影响正在进行的 dispach 过程。
如果没有 ensureCanMutateNextListeners 函数,在以下类型场景时就会出现异常(未定义错误/某些回调函数没有没执行):
let store = createStore(reducer);
let unsubscribe2 = ()=>{}
let unsubscribe1 = store.subscribe(() => {
console.log('Listener 1');
unsubscribe2()
unsubscribe1(); // 在 dispatch 过程中移除函数自己
});
unsubscribe2 = store.subscribe(() => {
console.log('Listener 2');
});
store.dispatch({ type: 'ACTION_TYPE' });
replaceReducer 的实现较为简单,传入新的 Reducer 替换原有 Reducer 后,再执行一遍 state 初始化逻辑,使得 state 更新为对应新 Reducer 的结果。
在开发某些大型应用的过程中,可能会根据不同的路由或模块动态加载 reducer。这样可以减少初始加载时间,提高应用性能。当新的模块加载时,可以使用 replaceReducer 替换当前的 reducer。Redux Dev Tools 也有使用这个功能进行状态回放。
function replaceReducer(nextReducer: Reducer<S, A>): void {
currentReducer = nextReducer as unknown as Reducer<S, A, PreloadedState>
dispatch({ type: ActionTypes.REPLACE } as A)
}
在应用逻辑比较复杂时,对应需要管理的 Reducer 和 state 状态也会增加,使用 combineReducers 可以将 Reducer 根据业务逻辑拆解为多个部分,每个部分独立维护整体 State 的一部分,以此降低应用的开发复杂度。
仅保留核心代码逻辑的 combineReducers 函数如下。
export default function combineReducers(reducers: {
[key: string]: Reducer<any, any, any>
}) {
const reducerKeys = Object.keys(reducers)
const finalReducers: { [key: string]: Reducer<any, any, any> } = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
}
const finalReducerKeys = Object.keys(finalReducers)
return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: Action
) {
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
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)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
}
可以发现,combineReducers 其实是返回了一个新的 Reducers 函数(combination),当 dispach 触发时候,该函数会遍历执行每个模块的 Reducer,并更新对应模块的 State。每个 Reducer 返回的 state 对象也按照传入 combineReducers 中的 Key 进行命名:
rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// 这将返回如下的 state 对象
{
potato: {
//
tomato: {
}
}
上文介绍 dispach 函数功能时有提到,dispatch 仅支持普通对象,而在实际开发中往往需要对 dispatch 做拓展,以支持 dispatch Promise 等其他类型的 action 对象,或是添加一些日志上报、数据缓存等操作。
applyMiddleware 就是为了解决在使用 Redux 过程中需要高频创建、组合多种对 dispach 的拓展的需求 ——> 在 dispach action 到执行 reducer 的过程中,先执行一系列的 middleware,类似 Express、koa 等框架的中间件概念。
View->Actions->[middleware1]->[middleware2]->Dispacher->Store->View
applyMiddleware 的源码如下:
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// store.dispatch 是作为最后一个执行函数的 next :最右边那个
//中间件执行顺序从左到右,洋葱模型。 所以在最左边的函数执行完 next 后,所有 state 即更新完毕
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
分析 applyMiddleware 的实现我们可以发现:applyMiddleware 本质上是返回了一个 enhancer 函数,该函数对 createStore 返回的 dispatch 函数做了“增强”。
一个典型的 middleware 如下,该 middleware 是一个三层嵌套函数,在 dispaching 过程中执行 log 数据的操作。结合上述 applyMiddleware 的实现我们可以发现,middleware 外层的 2 个函数都是在执行 applyMiddleware 的过程中就被调用了,最内层函数才会在 dispach action 后执行,因此实际的操作代码需要写在最内层函数中。
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// 实际操作代码需要写在最内层函数这里
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
}
}
const middlewareEnhancer = applyMiddleware(exampleMiddleware)
// 将 enhancer 为第二参数,因为没有 preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
compose 函数是实现 applyMiddleware 的关键,这个方法和 lodash 中的 flowRight 方法类似。
function compose(...funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return (arg:) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)
}
在 compose 函数中,通过 reduce 方法实现了从右到左组合多个函数功能:compose(f, g, h) -> (...args) => f(g(h(...args)))。
而 middleware 按传入顺序从左到右执行的原因在于,传入 compose 的 chain 是一系列二层函数,所以调用新 dispatch 函数后最先执行的是 chain 中的第一项(因为第一项在 compose 函数中会最后执行返回),而第一项的函数中又通过 next() 调用了第二项。... 进而实现了 middleware 的从左到右的依次调用。
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return createStore => (reducer, preloadedState) => {
// 省略其他逻辑
// chain:[fun1(next){ return fun11(action){next()} },fun2(next){ return fun22(action){next()} },fun3(next){ return fun33(action){next()} }]
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// dispatch : (action) => fun11(action){
// fun22(action){
// fun33(action){
// }
// }
// }
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
我们可以从日志上直观感受下执行顺序:
import { createStore, applyMiddleware } from "redux";
const initialState = {
count: 0
}
function reducer(state = initialState, action: { type: string}) {
switch (action.type) {
case 'add':
return {
...state,
count: state.count + 1
}
case 'reduce':
return {
...state,
count: state.count - 1
}
default:
return initialState;
}
}
const logger1 = storeAPI => next => {
console.log('logger1 执行返回最内层函数')
return action => {
console.log('logger1 开始');
const result = next(action)
console.log('logger1 结束');
return result
}
// @ts-ignore
const logger2 = storeAPI => next => {
console.log('logger2 执行返回最内层函数')
return action => {
console.log('logger2 开始');
const result = next(action)
console.log('logger2 结束');
return result
}
// @ts-ignore
const logger3 = storeAPI => next => {
console.log('logger3 执行返回最内层函数')
return action => {
console.log('logger3 开始');
const result = next(action)
console.log('logger3 结束');
return result
}
const middlewares = applyMiddleware(logger1, logger2, logger3);
const store = createStore(reducer, middlewares);
store.dispatch({ type: 'add' });
上方代码的输出依次为:
// 初始化后立即输出
logger3 执行返回最内层函数
logger2 执行返回最内层函数
logger1 执行返回最内层函数
// 触发 dispach 后输出
logger1 开始
logger2 开始
logger3 开始
logger3 结束
logger2 结束
logger1 结束
从 dispach 触发后的日志上也可以看出这是一个经典的洋葱模型设计, Koa、Express 框架的中间件功能也使用了这个设计模式。
这个 API 在实际开发中使用较少,Redux 文档描述为“唯一使用场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。” 所以这个函数的作用我们可以简单理解为:减少冗余代码+重复逻辑。
结合实际案例理解,下面是一段使用 react-redux 中 mapDispatchToProps 方法的代码:
mapDispatchToProps((dispatch)=>{
return {
action1:(data) => dispatch( { type:'add',payload:data } )
/* 通常情况下都用 actioncreator 来生成 action,所以通常是下面的写法:
action:(data) => dispatch( actioncreator(data) )
*/
}
})
由于通常的写法下 action 都是由 actioncreator 来生成的,既然所有 action 函数的都有这样的操作,那我们可以设计函数来封装一下,于是就有了 bindActionCreators,写法变成了这样:
mapDispatchToProps =((dispatch)=>{
return {
...bindActionCreators({
action1:actionCreator1,
action2:actionCreator2 //这样写的花就不涉及 dispatch 等操作的明文编写了
},dispatch)
}
}
bindActionCreators 的核心实现逻辑是给每个 actionCreator 返回一个与 dispach 绑定的执行函数,该函数执行后,会自动 dispach 对应的 action。
export default function bindActionCreators(
actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
dispatch: Dispatch
) {
// 支持单个 actionCreators
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 省略校验逻辑
const boundActionCreators: ActionCreatorsMapObject = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
function bindActionCreator<A extends Action>(
actionCreator: ActionCreator<A>,
dispatch: Dispatch<A>
) {
return function (this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
}
}