近期业务代码架构面临前端数据层升级,作为“幕后推手”(误)之一的我立誓要将年久失修的数据层状态管理方案来一个彻头彻尾的大升级。
在架构升级过程中,我也调研了许多前端全局状态管理方案。当前市面上最流行的 React 全局状态管理方案之一便是 redux系 + react-redux 实现的 Flux 单向数据流框架。redux 负责数据层的增删改查的核心操作与订阅逻辑,react-redux 则负责把 redux 与 UI 层绑定。
本文对于 redux 来一个深入解析。为了探究 redux 的实现原理,并抱着学习的目的,特意记录下此文。
一、Redux基本原理简述与回顾
1.1 Redux基本原理
如上所述,Redux 是 Flux 单向数据流框架的一种具体实现,它提供了一套管理全局状态数据的机制。在Flux框架中,Store是一个全局状态数据的管理者,View(视图)中会根据页面的交互的同时绑定发出Action(通知)。Dispatcher(分派器)用于接收 Action 并修改全局 Store,进而更新 View。
1.2 Redux核心概念
在redux中,几个重要的核心概念分别是:action,state,store,reducer。
- Action
Action 是一个对象类型,是交互行为信息的抽象,它描述发生了什么。这个对象必须有一个 type 属性,对于对象里面的其他内容,redux 不做限制,但 payload 内容推荐符合 Flux Standard Action 规范。例如:
{
type: 'ACTION_TYPE',
payload, // action 携带的数据
}
- Action Creator
Action Creator 是一种通过工厂函数创建 Action 的方法,通常为一个函数,返回一个对象类型,即 Action。Action Creator 的目的主要是解决如果我们想根据不同的参数来生成不同动作,可声明为:
const createActionType = (num) => {
return {
type: 'ACTION_TYPE',
payload: num,
}
}
- Reducer
reducer 是用来控制 state 改变的函数,它接受两个参数,第一个是 state,第二个是 action,并返回计算之后新的 state。
nextState = reducer(prevState, action);
另外,reducer 必须是纯函数,因为仅有排除了副作用,才可以保证幂等性,即先前的 state 不会改变,新的nextState 是一个最新的快照。
- Store 与 State
State 是作用在全局的状态变量,代表了 Redux 的核心数据;而 store 是一个“总指挥官”,
它负责:
- 通过 store.getState() 方法访问到托管的 state
- 通过 store.dispatch() 方法来触发 action 更新 state
- 通过 store.subscribe() 注册或 unsubscribe() 注销监听函数,监听每一次的 action 触发
const unsubscribe = store.subscribe(listner); // 注册
unsubscribe(); // 注销
综合来说,我们可以总结出:
- 只能有唯一的 store 对象保存整个应用的 state
- state 是只读的,只能通过 dispatch(action) 的方式来改变 state
- reducer 必须是纯函数
1.3 几个疑问
在了解 Redux 的原理并且稍加实践后,感叹全局数据管理便利的同时也产生了以下几个疑问:
- 状态变量初始化如何让全局都能访问到?
- 在 dispatch 一个 Action 后,Redux 是如何改变 state 的,它又如何能感知 state 的变化?
- 通过 createStore 创建“指挥官” store 的过程中,究竟是怎么实现的呢?
本文接下来的部分会对核心API的原理与源码进行分析,带大家一探究竟。
二、原理与核心API解析
2.1 createStore
createStore 是整个 redux 系统的引擎,也是最重要的 API,它负责创建一个持续存在于项目之中去且可被访问的store,通过闭包的方式储存 state 数据,并提供了一系列操作 store 的方法。为了了解整个 redux 是如何进行Action分派,state 属性更新,UI 渲染等,就让我们先来看看这个 API 源码。
由于 createStore 函数较长,我们这里先来看一下函数的大体架构:
export default function createStore(reducer, preloadedState?, enhancer?) {
/** 类型检查:createStore里面只能传入一个reducer function,若输入的preloadedState和enhancer都是函数则抛出错误
/** 类型转换:只传入了preloadedState(函数),就将这个preloadedState复制到enhancer上 */
if (typeof enhancer === 'function') {
// 有中间件,直接调用,强化createStore方法,返回增强后的store
return enhancer(createStore)(
reducer,
preloadedState
)
}
/** 类型检查:传入的reducer必须是函数 */
let currentReducer = reducer // 函数,传入的reducer,接收现有state,根据传入的actiontype返回新的state
let currentState = preloadedState // 传入的state,或者说是初始state
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false // 是否正在dispatching;正在dispatching过程中无法通过getState获取state
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice() // slice方法相当于浅拷贝数组
}
}
/** 获取当前state */
function getState() {/** */}
/**
* 本质上subscribe就是把传入的listener装在nextListeners里面,返回一个“取消订阅”函数。
* 这个“取消订阅”函数调用后,会将listener从nextListeners里面删除,然后清空currentListeners
*/
function subscribe(listener: () => void) {/** */}
/** 分发Action,输入一个Action,返回Action本身,有副作用 */
function dispatch(action) {/** */}
/** 替换掉当前用于计算state的reducer */
function replaceReducer(nextReducer) {/** */}
function observable() {/** */}
/**
* 创建store的时候,会分派一个INIT动作
* INIT action的作用:每一个reducer都返回他的初始状态,加起来以构成整个状态树
*/
dispatch({ type: ActionTypes.INIT })
const store = {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
return store;
}
可以看到,createStore 函数接收三个参数:1)reducer:一个 reducer 函数,提供更新 state 的逻辑,务必要保证是一个纯函数;2)preloadState:一个 state 对象,通常为项目初始的 state 变量;3)enhancer,中间件的入口,函数类型,若传入了中间件函数,则直接调用 enhancer(createStore)(reducer, preloadState)。有关中间件的介绍本文不多赘述,原理大致是用 enhancer(中间件)对 createStore 方法封装一层,生成一个强化版的createStore。
经过类型检查、转换、局部变量声明等过程后,createStore返回 redux 中的全局 store,这个 store 包含以下几个方法:
- dispatch:即 redux 中 dispatch 方法,用于分派 Action
- getState:获取 redux 当前状态下的 state
- replaceReducer:替换当前 redux 状态下使用的 reducer 函数
- subscribe:订阅并注册一个方法,每次 dispatch 触发后调用这个方法
下面我们逐一介绍一下 store 及其核心方法:
2.1.1 getState
getState 的作用是获取当前状态下运行在 redux 中的 state:
let currentReducer = reducer // 函数,传入的reducer,接收现有state,根据传入的actiontype返回新的state
let currentState = preloadedState as S // 传入的state,或者说是初始state
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false // 是否正在dispatching;正在dispatching过程中无法通过getState获取state
function getState(): S {
if (isDispatching) throw new Error(/** 分派过程中不能调用 */)
return currentState as S
}
这个函数的源代码较为简单,在 createStore 中定义了4个局部变量:
- currentReducer:当前的 reudcer,支持通过 store.replaceReducer(newReducer) 方法动态替换 reducer,为热替换提供了可能。
- currentState:当前状态下的 redux 的 state,默认为 createStore 传入的 preloadedState,即初始化的 state。
- currentListeners:当前 store 的监听器函数数组。
- nextListeners:临时监听器数组,用于 subscribe/unsubscribe 等对监听器的操作。
- isDispatching:某个 action 是否处于分发处理过程中。
2.1.2 dispatch
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(/** 必须是Object类型 */)
}
if (typeof action.type === 'undefined') {
throw new Error(/** Action必须是合法类型 */)
}
if (isDispatching) {
throw new Error(/** 不能正在dispatching */)
}
try {
// dispatch开始
isDispatching = true
// 利用当前的state,计算出新的state,并更新currentState
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 遍历监听器队列,依次调用。同时更新currentListeners为nextListeners的值
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
从上面的代码中可以看到,看似神秘的 dispatch 其实就要做了两件事:更新了 state、调用了监听器。我们知道,在 redux 设计理念中,dispatch 是唯一能够修改 state 数据的行为。dispatch 更新 state 的方式,就是通过当前 state 传入 reducer,配合 dispatch 函数传入的 action 及其 payload 计算得到新的 state,并更新到闭包数据中,这样就实现了 state 的更新。同时,为了实现 state 更新的同时完成一些动作(例如更新 UI 层),所有被注册的订阅函数都会执行
2.1.3 replaceReducer
随着业务逻辑复杂性的增加,项目中业务组件以及 state 会越来越多。如果我们使用 redux-router 结合 webpack,就可以使用按需加载进行减包。同理,在初始化 store 的时候,我们可以初始化一些公共的reducer,当加载到某个特殊页面后,再将对应所需的特殊 reducer 注入到 store 中。如果我们可以在状态数据层面实现按需加载,优化项目运行于构建的性能。
store 中的 repaceReducer 方法就提供给我们了这样的方法。它接收一个新的 reducer 函数,替换当前的 reducer,并且同时 dispatch 一个 ActionTypes.REPLACE 的 Action,用于初始化并更新 store 的状态:
function replaceReducer<NewState, NewActions>(
nextReducer: Reducer<NewState, NewActions>
) {
if (typeof nextReducer !== 'function') {
throw new Error(/** 传入的reducer必须是函数 */)
}
currentReducer = nextReducer
// 类似ActionTypes.INIT,
dispatch({ type: ActionTypes.REPLACE } as A)
return store;
}
可以看出,只要在加载到对应页面后,通过 replaceReducer 将该页面对应所需的 reducer 动态注入到全局 store 中,就实现了 reducer 的“按需更新”。
2.1.4 subscribe
上述所有 store 的方法本质上都是在对 store 的闭包数据以及局部变量进行操作。但这里还有一个问题,store 中的数据进行改变后,如何让 React,也就是 UI 页面与业务组件感知到呢?
我们知道,从 react 层面来说,redux 的 store 是隔离开的,我们需要一个桥梁,使得数据层出现更新的同时更新UI层逻辑,这时 store 中的最后一个方法,subscribe 方法就派上用场了:
function subscribe(listener: () => void) {
if (typeof listener !== 'function') {
throw new Error(/** listener必须是函数 */)
}
if (isDispatching) {
throw new Error(/** 分派过程中,禁止subscribe */)
}
// 除非调用unsubscribe,否则isSubscribed字段为true
let isSubscribed = true
// 给nextListners复制一遍currentListener,然后把传入的listener push进去
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// isSubscribed为false说明已经取消订阅过了,无需再次取消
if (!isSubscribed) return;
if (isDispatching) {
throw new Error(/** 分派过程中,禁止unsubscribe */)
}
isSubscribed = false
// nextListeners 复制currentListners,然后去掉传入的listner,设置currentListeners为空
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
从源码角度来看,subscribe 方法相当好理解,它主要的功能就是维护一个监听器队列。它将传入的 listener 函数添加到这个监听器队列中,同时返回了一个 unsubscribe 函数。当我们不在希望订阅这个监听器时,调用 unsubscribe,对应的函数就会从监听器队列中被移除。
subscribe 在传统的 redux 项目中,我们可以订阅如下的方法,在每次发生 dispatch 动作更新 state 时,用更新的 state 数据对当前应用进行渲染。这里需要使用观察者模式,订阅数据的改变,然后自动 render。
store.subscribe(() => renderApp(store.getState()))
对于这种通用需求,几乎所有高级的 redux 库都有对应的自动渲染方法,以保证数据层与 UI 层的同步。例如 react-redux 的 connect 方法,通过 mapStateToProps 参数为 react 组件注入 state。我们知道,当 react 组件中的 props 发生改变,就会重新渲染,并调用 componentDidUpdate() 方法。这样,就可以实现 dispatch 更新状态后自动对 UI 层进行渲染。
2.2 combineReducers
前面说过,reducer 作为 redux 框架中最核心的部分之一,承担了更新 state 的重任。然而在大型前端项目开发过程中,往往会将 state 拆解至不同 namespace 作用域,并分别通过不同 reducer 管理对应的状态。在 redux 中,整个应用只能有一个 store,即 createStore 方法只能调用一次,故我们需要一个方法,将分管不同作用域的 reducer 合并起来。
redux 核心 API 中提供了 combineReducers 方法,恰好为我们提供了上述问题的解决方案。这个方法接收一个名为reducers 的 Object,其中键与值分别对应不同 reducer 名称以及 reducer 函数。combineReducers 筛选出合法的键值对,通过闭包的形式返回一个函数。这个函数是一个统一的 reducer,它合并了作为参数传入的 Object,即 reducers。换句话说,这个返回的函数集合了所有分管不同作用域的 reducer,把它作为参数放到 createStore 里,就大功告成啦。
根据上面的描述,结合源码来看看:
export default function combineReducers(reducers: ReducersMapObject) {
const finalReducers: ReducersMapObject = /** 从传入的参数里面提取出合法的reducers(reducers的每一个key对应的value值是函数,才是合法的子reducer) */
/**
* 校验finalReducers, 判断其每一个子reducer是否能返回正常的子state
*/
const finalReducerKeys = Object.keys(finalReducers)
// 用于保证不会重复警告
let unexpectedKeyCache: { [key: string]: true }
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError: Error
/** 省略部分代码:确认过滤后的finalReducers能返回正常的子state(即不返回undefined,一定返回的是state对象) */
/**
* 返回一个函数combination,这是一个标准的reducer函数,有初始化的state参数,和一个携带了actionType和数据的action对象。
*/
return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: AnyAction
) {
/** 省略部分代码:如果reducers不合法,则这个函数调用后会直接抛出错误;传入state格式不匹配,提示报警 */
let hasChanged = false
// 定义新的nextState
const nextState: StateFromReducersMapObject<typeof reducers> = {}
/**
* 这个循环作用就是把筛选出来的有效reducers按照从上到下的顺序执行一遍。
* 在每一次运行中,如果reducer执行后生成的state与传入的state不同,说明需要返回新的state(nextState)
* 最后返回一个stateMap对象,其中key还是原来传入state的key,value是执行reducer输出的state
*/
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为true,那就是true了
// 后面的判断是,只要有一次nextStateForKey!== previousStateForKey不同,就说明整个state不同
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
//如果state发生变化了,直接返回新的nextState,否则,还是返回旧的state
return hasChanged ? nextState : state
}
}
2.3 bindActionCreators
bindActionCreators 方法是核心 API 中另一个辅助方法,它提供一种通过方法调用 Action 的方式。具体来说,就是原本以对象形式出现的 Action,通过该方法可以将其封装成相对应的 Action Creator,同时这个 Action Creator 被调用的时候将会自动 dispatch。
之前说过,action 是一个包含 type 属性的纯对象,派发一个 action 需要调用 dispatch 方法。派发操作是非常频繁的,如果每个 react 组件都引入 store 再派发 action 会显的很冗余。例如我需要购买 iMac,我们可以将其设置为一个 BUY_iMAC 的 Action:
const buyMacActionCreator = (payload) => ({ type: 'BUY_iMAC', payload })
const action1 = buyMacActionCreator(3);
dispatch(action1) // { type: 'BUY_iMAC', payload: 3 }
const action2 = buyMacActionCreator(5);
dispatch(action2) // { type: 'BUY_iMAC', payload: 5 }
这里,我们想依此采购3个和5个 iMac,相当于依次分发值为3、5,类型为 BUY_iMAC 的三个 Action。可见每次dispatch 都需要手动调用 buyMacActionCreator 来创建动作。利用API中的工具函数 bindActionCreator 来优化代码了,该函数的源码如下:
/**
* 闭包,直接返回一个函数,这个返回出来的函数的作用是:
* 传入的arguments,生成构建出对应的Action,然后把这个Action分派
*/
function bindActionCreator(actionCreator: ActionCreator, dispatch: Dispatch) {
// 执行后返回结果为传入的actionCreator直接调用arguments
return function (this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
}
}
可见,bindActionCreator 返回了一个匿名函数,其作用是:1)生成 Action,2)分派 Action。利用这一方法,我们可以优化上述案例为:
/** 省略上述代码 */
const buyMac = bindActionCreator(buyMacActionCreator, dispatch)
buyMac(3) // { type: 'BUY_iMAC', step: 3 }
buyMac(5) // { type: 'BUY_iMAC', step: 5 }
大型 redux 项目中,往往会出现需要合并多个类型 Action 的场景,这时 bindActionCreators 便派上用场了。先来看看代码实现:
/**
* 如果传入的是ActionCreator函数,那么直接调用bindActionCreator,返回一个函数
* 如果传入的是ActionCreators对象,那么构建一个新对象,其中key保持不变,value则是对应的ActionCreator调用bindActionCreator后的结果
* 即对于传入的每一个ActionCreator函数,返回一个Object,key对应函数。
* 这里每一个value的函数,都可以接收args,然后实现分派动作。
*
* 使用bindActionCreators方法的目的主要是为了将多个Actions与对应的dispatch合并成一个Object,或者是某个Action对应dispatch有很多参数
* 时,只需要在传入的actionCreators中修改即可(相当于只用修改Action),bindActionCreators会自动返回一个函数以实现dispatch所有参数。
* 特别是在mapDispatchToProps中,无需手动修改其他Reducer和connect/mapDispatchToProps函数。
*
* 总结一下,dispatch 和 bindActionCreators 归根到底,其作用是一样的。
* 但如果 actions 需要传入多个参数时,我们可以借助 bindActionCreators 来精简我们的代码。
*/
export default function bindActionCreators(
actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
dispatch: Dispatch
) {
// actionCreators为function
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 不是object,throw Error
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(/** 错误信息:必须是Object类型 */)
}
const boundActionCreators: ActionCreatorsMapObject = {}
for (const key in actionCreators) {
// key为actionCreators的方法名
// actionCreator为对应的创建Action的工厂函数
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
// 调用bindActionCreator以合成ActionCreator和dispatch动作
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
总之,bindActionCreators 就是将多个 actionCreator 合并且实现在调用时自动 dispatch。例如:
const buyMacActionCreator = (payload) => ({ type: 'BUY_iMAC', payload })
const buyIPhoneActionCreator = (payload) => ({ type: 'BUY_iPhone', payload })
const earnMoneyActionCreator = (payload) => ({ type: 'EARN_MONEY', payload })
const mapStateToProps = {
// ...
}
const mapDispatchToProps = (dispatch: Dispatch) => {
...bindActionCreators({
buyMacActionCreator,
buyIPhoneActionCreator,
earnMoneyActionCreator,
}, dispatch),
}
@connect(mapStateToProps, mapDispatchToProps)
class Component extends React.Component {
public render() {
return // ...
}
private buyMac(number) {
// 可以触发 BUY_iMAC Action 并 dispatch 进入 redux store
this.props.buyMacActionCreator(number);
}
private buyIPhone(number) {
// 可以触发 BUY_iPhone Action 并 dispatch 进入 redux store
this.props.buyIPhoneActionCreator(number);
}
private earnMoney(number) {
// 可以触发 EARN_MONEY Action 并 dispatch 进入 redux store
this.props.earnMoneyActionCreator(number);
}
}
上面的例子通过 react-redux 将分发事件的逻辑绑定在 UI 组件中,组件 Component 获得了 buyMacActionCreator、buyIPhoneActionCreator、earnMoneyActionCreator 三个能将数据写入 redux store 的方法,并绑在了组件的 props 上。
2.4 applyMiddleware
redux 中间件类似于装饰器,其实目的只有一个:在 dispatch 前后,执行一些代码逻辑,以达到增强 dispatch 的作用。上面说过,reducer 设计理念应为纯函数,而若我们需要一些副作用(例如打印日志),中间件的作用就体现出来。applyMiddleware 就是一个能提供中间件的API,通常其返回值作为 createStore 的第三个参数 enhancer 传入。
if (typeof enhancer === 'function') {
// 有中间件,直接调用,强化createStore方法,返回增强后的store
return enhancer(createStore)(
reducer,
preloadedState
)
}
/** 剩余createStore的代码 */
而 applyMiddleware 方法作为 enhancer 传入的案例,我们可以看下下面的例子,它引用了一个非常经典的 redux-logger 中间件,用于提供打印日志的功能
import { applyMiddleware, createStore } from'redux';
import { createLogger } from'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
preloadStates,
applyMiddleware(logger),
)
这里有一个有意思的现象,当我们在 createStore 方法中传入 middleware 后,createStore 方法实际上将会被执行两次:第一次会在
typeof enhancer === 'function'
判断的时候再次进入 createStore 方法。此时,dispatch 已经被强化。进而接下来的 createStore 创建的全局 store 便拥有了我们想要给其增强的能力。按照 createStore 中的定义规范,applyMiddleware 方法返回一个函数。在创建 store 的时候实际上走的是 applyMiddleware(logger)(createStore),在接下来的代码片段中我们可以看到 createStore 方法会被第二次进入,但这时仅用到了 reducer 和 preloadStates 两个参数,enhancer 会被置为 undefined,因为先前已经调用过 applyMiddleware(logger) 这个 enhancer 啦。
中间件对于 redux 来说是灵魂一般的存在,它给 redux 提供了极强的拓展性,作为中间件的入口,让我们来看看这个 API 的实现:
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreEnhancerStoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => {
/**
* 直接原封不动的执行createStore,生成原始的store。
* 这一步骤其实和普通的createStore别无两样,middlewares主要其作用的地方是替换掉了这个store的dispatch方法
*/
const store = createStore(reducer, preloadedState)
let dispatch: Dispatch = () => {
throw new Error(/** 不能在创建middleware时dispatch */)
}
// 这里约定了所有的redux中间件只能使用的api就是2个,getState和dispatch
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
// 对每一个中间件,执行中间件函数,并将中间件api对象传入函数,用chain变量接收中间件执行结果的其返回值
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 分别按顺序从后往前执行每一个中间件,并且前一个中间件的返回值作为后一个中间件的入参,所有中间件的第一个入参是原始的store.dispatch
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
// 最后返回store对象,区别是使用了经过中间件加工过的dispatch对象,替换掉原始的dispatch对象
return {
...store,
dispatch,
}
}
}
applyMiddleware 返回了一个可作为 enhancer 参数的函数,这个函数通过闭包数据 middlewares 数组来强化传入的 createStore 方法,进而起到增强 store 和 dispatch 的能力。可以看到,返回函数中创建了 store,同时使用将配置的中间件一层一层地叠加,并用最终结合了所有中间件能力的一个方法增强 dispatch 函数,以起到我们想要的附加效果。
2.5 compose
compose 函数作为工具函数,用于将传入的一系列函数组合成一个复合函数。这个函数就是把一个函数数组按照顺序从最后向最前顺序执行,并且将前一个执行函数结果的返回值当作下一个执行函数的入参。
export default function compose(...funcs: Function[]) {
// 没传参数,则什么都不做,即传入什么,就返回什么的函数
if (funcs.length === 0) return <T>(arg: T) => arg
// 参数长度为1,则将参数列表中的第一个函数作为返回值
if (funcs.length === 1) return funcs[0]
/**
* 核心代码:对funcs列表执行reduce函数
* reduce方法将(...args) => a(b(...args))整体作为返回值,赋于下一次迭代。
* 比如funcs = [f1, f2, f3, f4], 执行流程如下
* a1 = (...args) => f1(f2(...args))
* a2 = (...args) => a1(f3(...args))
* a3 = (...args) => a2(f4(...args))
*
* 依次代入,则得到
* a1 = (...args) => f1(f2(...args))
* a2 = (...args) => f1(f2(f3(...args)))
* a3 = (...args) => f1(f2(f3(f4(...args))))
*/
return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}
三、扩展:突破redux的限制
3.1 与View层的结合: react-redux
前面说过,redux 是一套管理数据层的框架,而它与我们的React项目是隔离的,也就是说,redux 并不能够感知到React,也就无法直接搭建起通信的桥梁。传统的解决方案是将 React 的 render 方法加入 redux 的订阅中,使得在每次 dispatch 后都进行重新渲染以实现。而 React-redux 的出现则有效地解决了这个问题。
React-redux是基于容器组件与UI组件分离的思路设计的一套提供给 React 使用的绑定库。结合我们工程内的应用,这里简要介绍两个 API:
-
mapStateToProps:将 store 中的 state 映射到组件或页面的 props 中,使得当组件内访问 props 中的数据时,它们指向的是全局state中的数据。
-
mapDispatchToProps:将 store 中各种 dispatch 方法映射到组件或页面的 props 中,使得当组件内调用 props 对应的方法时,对应的 Action 会被dispatch。
接上面 bindActionCreators 的例子,分派edit/addQuestion这一Action的动作被绑定在了props.addQuestion(payload)方法下,常用的操作就是将这个方法绑定到 DOM 事件上(例如某个按钮的点击)。同时,consume.iphoneNum, produce.money 等 state 状态也被映射到了 props 中。当 DOM 事件被触发后,state 连同组件的 props 相应更新,页面重新渲染,也就形成了从交互到数据层再到 UI 层的闭环。
3.2 解决异步Action与副作用: redux-saga/dva
以上的介绍我们可以看到,redux 及核心 api 的作用其实还是比较局限的,例如 reducer 作为一个更新 state 的纯函数,并不支持例如异步 Action、发送 AJAX 请求等带有副作用的操作。而在我们的工程中,有大量需要在网络请求拉回数据后更新应用数据层的逻辑。
3.2.1 Redux-saga
Redux-saga 提供了一种解决方案:通过 redux 的中间件方式包装 store,通过被实现为Generator函数的 saga 来执行带有异步等副作用的操作。
saga 会 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。在这种场景下,调用dispatch的过程也会被封装成 yield 操作,例如 put 方法:
put({
type: 'INCREMENT',
payload: data,
})
put 在 redux-saga 中被称为 Effect ,是一些简单 Javascript 对象,包含了要被 middleware 执行的指令,它会暂停 Saga,直到 Effect 执行完成。这个方法相当于调用了dispatch,将 INCREMENT 的 Action 对象传入 dispatch,执行分发操作。唯一不同的地方,就是通过 saga,我们可以控制 dispatch 的顺序,即实现在完成异步请求拿到 AJAX 回调数据后,再进行 dispatch 操作。
3.2.2 dva
umi 框架作者开源的 dva 也是当前流行的数据流管理框架,它其实是对 redux + react-redux + redux-saga + react-router 的封装。redux 负责数据层管理,react-redux 负责 UI 层与数据层的绑定,redux-saga 负责编写并处理异步逻辑,react-router 负责控制路由。
有关 redux 的异步数据流方案,还有 redux-thunk 等中间件方案,甚至于许多轻量的开源框架 concent、remesh、recoil 等都天然支持异步 reducer,作为新时代数据流解决方案,异步 reducer 也是必备的操作了,这里就不多介绍了。
redux-saga 和 dva 相对来说引入了不少新概念,给写惯了 async await 的前端开发们带来了不少心智负担,有关这两个框架的源码分析也已经安排上了,近期也将呈现给大家~
四、小结
本文简单记录了阅读Redux源码过程中的一些心得与总结。redux作为一个框架,更是一种前端工程中数据管理的设计理念。后续我也会安排上上文提到的 redux-saga,dva 等状态管理方案源码阅读与解析,希望感兴趣的小伙伴们能喜欢。
最后,第一篇掘金文章,还希望各位大佬能多提些意见,多多交流,互相进步互相学习。