好些个日子没有写博客了,脑子里头想着有好些事情该干的该写的,但平日里就又陷入实验室与课程所带来无穷的琐事中了。偶尔偷得闲,还是写两篇罢,老是囤在笔记里头日晒不着的,眼瞅着就要发霉生虫了。
Redux源码的设计简介明白,拢共加起来也不到千行,注释写的也清清楚楚,只要心里头对于Redux的设计理念有个大致的了解,源码就能很轻松的阅读。
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。
官网的这两句介绍就把Redux的来龙去脉介绍的一清二楚了,要理解Redux的设计思路,首先得对Redux的核心概念和函数式编程思想有一定的认知。
这儿就不对这些预备知识进行介绍了,Redux文档中已经给出了非常细致的介绍(其实Redux文档中涵盖了方方面面的介绍,例如核心思想、使用技巧、API文档等,这些内容比单单分析Redux源码更有价值)。函数式编程在Redux源码中大概体现在Currying、Compose以及纯函数,只需花10分钟简单了解即可。
首先来看一眼Redux源码的文件结构:
文件结构很清爽,包含了6个核心文件和3个工具文件,简单列个表来看看其大致功能:
文件 | 功能 | 备注 |
---|---|---|
index.js |
入口文件 | |
createStore.js |
提供 createStore API | |
compose.js |
函数组合 | 虽然也是工具文件的功能但是不知道为什么不放在 utils 目录下 |
combineReducers.js |
提供了 combineReducers API,同时包含了一些 reducer 的验证函数 | |
bindActionCreators.js |
提供了 bindActionCreators API | |
applyMiddleware.js |
提供了 applyMiddleware API | |
warning.js |
显示 warning 提示信息 | 工具文件 |
isPlainObject.js |
判断对象是否是一个普通对象 | 工具文件 |
actionTypes.js |
提供预设的 action | 工具文件 |
工程中所使用的工具文件包含在 utils
文件夹下(compose.js
也算在里头),简单介绍其中的一部分:
// compose.js
// Redux 中 compose 函数和函数式编程中是同个概念,就是对函数进行组合
// compose(f, g) --> f(g)
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)))
}
// actionTypes.js
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`, // store 初始化所触发的 action
REPLACE: `@@redux/REPLACE${randomString()}`, // store 中 reducer 被热替换所触发的 action
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` // 未知的 action
}
actionTypes 中所定义的三个预设 action 主要在 createStore
以及 combineReducers
中有所使用,详见后续章节内容。
createStore
是Redux所提供的API之一,其用于生成唯一的 store 并提供对应的 dispatch
、subscribe
等方法。Redux 中的 store 仅能通过 dispatch
函数触发对应的 action 来改变,action 对应到纯函数的 Reducer 从而修改 store 的数据。createStore
的一个使用示例如下:
let store = createStore(myReducer, {}, applyMiddleware(thunk, logger));
createStore
接收三个参数:reducer, preloadedState, enhancer
,其中reducer
参数表示对store进行修改的 reducer 函数,其可由combineReducers
函数组合多个 reducer 生成。preloadedState
代表 store 的初始化状态,该参数可以省略,当其被省略时,输入参数为:reducer, enhancer
。enhancer
是中间件通过applyMiddleware
方法所生成的对createStore
进行功能加强的函数。具体代码解析如下:
export default function createStore(reducer, preloadedState, enhancer) {
// 处理传入多个 enhancer 的错误
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error('balabala')
}
// 当传入两个参数时,其输入参数的含义为 (reducer, enhancer)
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
// enhancer 必须是一个函数
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 使用 enhancer 对 createStore 功能进行加强,enhancer 的具体执行功能见 applyMiddlerware
return enhancer(createStore)(reducer, preloadedState)
}
// reducer 同样必须为函数,为什么不把这一步判断提前到 enhancer 前头?
// 这样不就留下了到 enhancer 执行完后再处理了吗?
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer // 保存 store 对应的 reducer
let currentState = preloadedState // 创建时的当前状态即为初始状态
let currentListeners = [] // 记录对于 store 状态变化的 listeners
let nextListeners = currentListeners /// 通过两个数组保存 listerners 以保证执行过程的有序,详见后续说明
let isDispatching = false // 是否正在 dispatch 的标识符
// 用于保证 nextListeners 和 currentListeners 不会指向同个数组
function ensureCanMutateNextListeners() {
// 当 nextListeners 和 currentListeners 指向同个数组时,通过 slice 来创建一个副本
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 返回当前 state
function getState() {
// 当 dispatch 正在执行时报错
if (isDispatching) {
throw new Error('balabala')
}
return currentState
}
// 对 store 的变化进行订阅
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
// 同样不允许在 dispatch 执行过程中订阅
throw new Error('balabala')
}
let isSubscribed = true
// 保证 nextListeners 和 currentListeners 不指向同个数组,
// 同时,所有的新添加的 listener 都放到 next 数组中
ensureCanMutateNextListeners()
nextListeners.push(listener)
// 与常见的发布订阅的实现相同,返回一个取消订阅的方法
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error('balabala')
}
isSubscribed = false
// 与添加 listener 相同,仅在 next 数组上执行删除操作
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 触发 action 从而修改 store 数据
function dispatch(action) {
// action 必须是一个普通对象
if (!isPlainObject(action)) {
throw new Error('balabala')
}
if (typeof action.type === 'undefined') {
throw new Error('balabala')
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 执行 reducer 对 store 进行修改
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 结合 subscribe 中的相关代码,可以看到为了保证在对 listener 的一次遍历过程中,
// 其不会因为 listener 内部包含了订阅/取消订阅而使得调用顺序变得一团糟,执行过程中
// 所有对于 listeners 数组的操作均在下一次 dispatch 中才生效。
// 为此,使用两个 listener 数组是必须的。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// 对当前的 reducer 进行替换,以满足动态修改 reducer 的需求
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
// 提供与响应式库协同的入口
function observable() {
// balabala...
}
// 当 store 被创建时,触发 init 事件从而允许 reducer 执行一些初始化操作
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
createStore
函数中的逻辑比较简单,基本上就是依次创建各个API函数并包裹起来返回。
需要注意其中的nextListeners
以及currentListeners
两个内部变量,如果仅仅为了存储 listener 并对其执行调用的话,只需要一个数组就足够了,但是这里却使用了两个数组。事实上,listener 内部可能包含了 subscribe 或者 unsubscribe 的操作,仅在一个数组上操作可能会导致在一次 listener 的遍历过程中,其执行顺序变得一团糟。添加 nextListeners
数组后,listener 中的订阅/取消订阅操作仅在 nextListeners
上执行,并且在下一次遍历 listener 的操作前才生效,从而保证了执行过程的有序。
不过对于createStore
函数我仍存在一些疑问,对于 reducer
参数的判断放在了enhancer
执行之后,这样使得程序进入了enhancer
忙活了一堆之后才发现基本的reducer
还存在问题。虽然对于正确性和性能都没什么问题,但是终归看起来觉得不优雅。
bindActionCreators
函数用于多个 actionCreator 绑定到 dispatch 上使得调用更为便捷。
先分析绑定单个 action 到 dispatch 上的函数bindActionCreator
:
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
如果将其中的 actionCreator
参数用 f
替代,dispatch
参数用 g
替代并做一些简化可以看到:
function bindActionCreator(f, g) {
return (...args) => { f(g(...args)) }
}
其实bindActionCreator
就是对于 actionCreator 和 dispatch 两个函数的 compose。
对应的,bindActionCreators
也相类似:
export default function bindActionCreators(actionCreators, dispatch) {
// 单一函数直接调用 bindActionCreator ,不过这会导致函数返回结果类型的不一致
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error('balabala')
}
// map actionCreators数组,并分别做 compose,最终返回一个操作后的数组
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
// 仅对类型为 function 的 actionCreator 进行处理
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
具体实现没什么可说的,不过有一点要吐槽:bindActionCreators
针对于传入参数的类型的不同,其返回值也会随之改变。当传入的actionCreators
为一个函数时,返回值为函数;传入的actionCreators
为函数的数组时,返回值也是一个数组。这样的设计可能会导致草草看了一眼API文档的开发者掉进了返回值类型不同的坑里头去。
combineReducers
函数位于 combineReducers.js 文件中,里面同时还包含了一些验证 reducer 处理结果的相关函数。
combineReducers
函数用于对多个 reducer 进行整合生成一个的 reducer,直接来看代码:
// 对多个 reducer 进行整合
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 遍历 reducers 并将属于 function 的元素放入 finalReducers
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
// 判断 reducer 是否能够处理 init 事件以及未知事件
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
// 开发环境中预先对 reducer 处理结果进行判断并报 warning
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,finalReducers,action,unexpectedKeyCache)
if (warningMessage) {
warning(warningMessage)
}
}
// 遍历 finalReducer 并对应生成 stateForKey 并放入 nextState 中。
// preloadedState 中 key 不被包含在 finalReducer 中的 stateForKey 就被被抛弃了。
// 不过问题也不大,根本不会被 reduce 所操作的数据本身就应该在配置文件中写死。
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
}
}
combineReducers
函数的逻辑很简单,也没有使用 compose 等方法,直接将多个 reducer 保存在对象中返回一个函数用于对其进行遍历操作。
其中需要注意的是,由于combineReducers
所返回的函数处理逻辑,其不会在遍历开始之前就拷贝一份原先状态,而是根据 reducer 对应的 key 来依次添加 stateForKey 的数据。这也就意味着,preloadedState 的设定的初始 state 中的部分数据,如果其 key 没有对应到某个的 reducer,则会导致其在更新过程中丢失。不过从设计的合理性角度来讲,这一特性也不成问题,毕竟本身不对应到 reducer 的数据意味着其不可更改,应当在配置文件中写死。
(敲黑板:重点内容,大家拿出笔记一下)
最后是重头戏applyMiddleware
函数。这也是 Redux 允许开发人员为其应用插件的核心 API 。applyMiddleware
函数代码实现部分短小精悍,仅有 20 余行,但是理解起来还需稍微费些功夫。
为了理解applyMiddleware
函数的实现原理,首先应当了解其应用场景:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
applyMiddleware(logger)
上述代码中展示了一个最简单的单一插件应用实例。可以看到,插件是一个较为复杂的高阶函数。这里依次对其传入的参数为store
、next
和action
。
接下来,先转过头来看applyMiddleware
函数的实现:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 开头还是老样子,先把 store 按默认方法创建出来
const store = createStore(...args)
// 创建一个 dispatch 的临时版本,其在最终完成 dispatch 构建之前被调用则会抛出错误
let dispatch = () => {
throw new Error('balabala')
}
// 提供一个伪造的 store,包含了中间件可能用到的函数调用
const middlewareAPI = {
getState: store.getState,
// 这里的 dispatch 包裹了 compose(...chain)(store.dispatch) 所生成的 dispatch,
// 其用于再次遍历整个 middleware 链,对异步的 middleware 非常有用。
dispatch: (...args) => dispatch(...args)
}
// 注意这里的 chain 就是个数组的意思,和函数式编程的 chain 概念没有关联
// 将伪造的 store 绑定到中间件的闭包里,map 后生成中间件绑定后的数组
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 将所有的函数进行 compose 并将 dispatch 作为 next 绑定到闭包中,生成一个新的 dispatch。
// 实际的执行过程中,dispatch 将在最后一个中间件执行 next(action) 时被调用,
// 但是其结果会被一层层的回传到当前中间件位置。
// 中间件的执行顺序应该是:applyMiddleware(f, g) --> g -> f -> dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
看完注释可能你还是会觉得糊里糊涂,不打紧,我再对整个代码实现思路做一次详细的描述。
还记得createStore
函数中调用applyMiddleware
执行结果的方式吗:
enhancer(createStore)(reducer, preloadedState)
可以看出,applyMiddleware
函数实际返回的是一个以createStore
为参数的加强版createStore
函数,其对于dispatch 扩充了中间件所提供的功能。
结合使用示例以及源码,现在可以对中间件所使用的参数进行解读:
store
参数实际上传入的是 middlewareAPI
。其是一个伪造的 store,提供了类似于getState
以及dispatch
两个函数。next
参数则是指向其前一个中间件生成的函数。action
参数则是强后的 dispatch
函数被调用时传入的 action。具体而言:
传入store
的middlewareAPI
中的dispatch
函数事实上是使用了箭头函数对外部的dispatch
进行了一层包裹。这一设计的目的是为了防止中间件直接对dispatch
进行修改从而导致错误,同时也使得外部的dispatch
需要被预先声明。为了防止dispatch
在构建完成前被调用,在声明时为其赋值一个调用即报错的函数。
applyMiddleware
构建生成dispatch
函数的核心代码就两句:
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
第一行代码中,middleware 数组进行 map 操作,将middlewareAPI
保存在中间件的闭包中。此时,被调用一次后的中间件变为了:
next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
第二行代码中,保存了middlewareAPI
的中间件进行 compose 并直接传入 store.dispatch
执行。这一行的执行用代码表示如下:
// 假设 f 和 g 均为保存了 middlewareAPI 后的中间件
let f = next => action => {
console.log('middle f')
return next(action)
}
let g = next => action => {
console.log('middle g')
return next(action)
}
// 将 f 和 g 进行组合的结果为:
compose(f, g) --> (...args) => f(g(...args)) -->
((next) => (action => {
console.log('middle g')
return next(action)
})) => (action => {
console.log('middle f')
return next(action)
})
// 上述并非严格的代码,其表明了一个执行逻辑:先执行中间件 g 中的函数,后将其作为参数 next 传入 f 中执行
// 传入 store.dispatch 调用的结果为:
compose(f, g)(store.dispatch) --> f(g(store.dispatch)) -->
action => {
console.log('middle f')
return ((action => {
console.log('middle g')
return store.dispatch(action)
}))(action)
}
// 由于传入了 store.dispatch 参数并执行,所有的 next 参数都被传入函数内,并返回以 action 为参数的最底层函数
如上述代码所示, f
中间件的 next 为根据 g 中间件生成的函数;g
中间件的 next 为 store.dispatch
。如果还有不清楚的地方,就结合代码跑一跑看看罢,实践出真知嘛。
需要注意的是,实际上通过applyMiddleware
函数所加强的dispatch
函数,其对中间件的执行顺序是从右到左的。仍以上述代码为例,传入中间件顺序是:f, g
,但是实际执行完毕的顺序是:dispatch -> g -> f
。其原因为:传入store.dispatch
参数并执行的过程将函数 compose 后得到的头衔尾的参数传递关系解开了,从而暴露出了最外层的返回的以action
为参数的类 dispatch 函数。
做个简单的总结吧。
Redux 代码本身比较短小精炼,不像上万行的 React 代码看得人晕头转向。代码虽然简单,但是其背后的设计思想才是其最具价值的内容。此外,例如applyMiddleware
极简的实现也同样值得好好学习。
// 磨蹭了个把月才划掉日程表上的一项,我可得长点心了。(点心,什么点心)