很久之前就看过一遍 Redux
相关技术栈的源码,最近在看书的时候发现有些细节已经忘了,而且发现当时的理解有些偏差,打算写几篇学习笔记。这是第一篇,主要记录一下我对Redux
、redux-thunk
源码的理解。我会讲一下大体的架构,和一些核心部分的代码解释,更具体的代码解释可以去看我的repo,后续会继续更新 react-redux
,以及一些别的 redux
中间件的代码和学习笔记。
注意:本文不是单纯的讲 API
,如果不了解的可以先看一下文档,或者google
一下 Redux
相关的基础内容。
整体架构
在我看来,Redux 核心理念很简单
store
负责存储数据- 用户触发
action
reducer
监听action
变化,更新数据,生成新的store
代码量也不大,源码结构很简单:
.src
|- utils
|- applyMiddleware.js
|- bindActionCreators.js
|- combineReducers.js
|- compose.js
|- createStore.js
|- index.js
其中 utils
只包含一个 warning
相关的函数,这里就不说了,具体讲讲别的几个函数
index.js
这是入口函数,主要是为了暴露 Redux
的 API
这里有这么一段代码,主要是为了校验非生产环境下是否使用的是未压缩的代码,压缩之后,因为函数名会变化,isCrushed.name
就不等于isCrushed
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
warning(...)
)}
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
warning(...)
)}
createStore
这个函数是 Redux
的核心部分了,我们先整体看一下,他用到的思路很简单,利用一个闭包,维护了自己的私有变量,暴露出给调用方使用的API
// 初始化的 action
export const ActionTypes = {
INIT: '@@redux/INIT'
}
export default function createStore(reducer, preloadedState, enhancer) {
// 首先进行各种参数获取和类型校验,不具体展开了
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {...}
if (typeof reducer !== 'function') {...}
//各种初始化
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
// 保存一份 nextListeners 快照,后续会讲到它的目的
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function getState(){...}
function subscribe(){...}
function dispatch(){...}
function replaceReducer(){...}
function observable(){...}
// 初始化
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
下面我们具体来说
ActionTypes
这里的 ActionTypes
主要是声明了一个默认的 action
,用于 reducer
的初始化。
ensureCanMutateNextListeners
它的目的主要是保存一份快照,下面我们就讲讲 subscribe
,以及为什么需要这个快照
subscribe
目的是为了添加一个监听函数,当 dispatch action
时会依次调用这些监听函数,代码很简单,就是维护了一个回调函数数组
function subscribe(listener) {
// 异常处理
...
// 标记是否有listener
let isSubscribed = true
// subscribe时保存一份快照
ensureCanMutateNextListeners()
nextListeners.push(listener)
// 返回一个 unsubscribe 函数
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
// unsubscribe 时再保存一份快照
ensureCanMutateNextListeners()
//移除对应的 listener
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
这里我们看到了 ensureCanMutateNextListeners
这个保存快照的函数,Redux
的注释里也解释了原因,我这里直接说说我的理解:由于我们可以在listeners
里嵌套使用 subscribe
和 unsubscribe
,因此为了不影响正在执行的listeners
顺序,就会在 subscribe
和 unsubscribe
时保存一份快照,举个例子:
store.subscribe(function(){
console.log('first');
store.subscribe(function(){
console.log('second');
})
})
store.subscribe(function(){
console.log('third');
})
dispatch(actionA)
这时候的输出就会是
first
third
在后续的 dispatch
函数中,执行 listeners
之前有这么一句:
const listeners = currentListeners = nextListeners
它的目的则是确保每次 dispatch
时都可以取到最新的快照,下面我们就来看看 dispatch
内部做了什么。
dispatch
dispatch
的内部实现非常简单,就是将当前的 state
和 action
传入reducer
,然后依次执行当前的监听函数,具体解析大概如下:
function dispatch(action) {
// 这里两段都是异常处理,具体代码不贴了
if (!isPlainObject(action)) {
...
}
if (typeof action.type === 'undefined') {
...
}
// 立一个标志位,reducer 内部不允许再dispatch actions,否则抛出异常
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
// 捕获前一个错误,但是会将 isDispatching 置为 false,避免影响后续的 action 执行
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 这就是前面说的 dispatch 时会获取最新的快照
const listeners = currentListeners = nextListeners
// 执行当前所有的 listeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
这里有两点说一下我的看法:
- 为什么
reducer
内部不允许再dispatch actions
?我觉得主要是为了避免死循环。 - 在循环执行
listeners
时有这么一段
const listener = listeners[i]
listener()
乍一看觉得会为什么不直接 listeners[i]()
呢,仔细斟酌一下,发现这样的目的是为了避免 this
指向的变化,如果直接执行listeners[i]()
,函数里的 this
指向的是 listeners
,而现在就是指向的Window
。
getState
获取当前的 state
,代码很简单,就不贴了。
replaceReducer
更换当前的 reducer
,主要用于两个目的:1. 本地开发时的代码热替换,2:代码分割后,可能出现动态更新 reducer的情况
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
// 更换 reducer
currentReducer = nextReducer
// 这里会进行一次初始化
dispatch({ type: ActionTypes.INIT })
}
observable
主要是为 observable
或者 reactive
库提供的 API
,Reux
内部并没有使用这个API
,暂时不解释了。
combineReducers
先问个问题:为什么要提供一个 combineReducers
?
我先贴一个正常的 reducer
代码:
function reducer(state,action){
switch (action.type) {
case ACTION_LIST:
...
case ACTION_BOOKING:
...
}
}
当代码量很小时可能发现不了问题,但是随着我们的业务代码越来越多,我们有了列表页,详情页,填单页等等,你可能需要处理
state.list.product[0].name
,此时问题就很明显了:由于你的
state
获取到的是全局
state
,你的取数和修改逻辑会非常麻烦。我们需要一种方案,帮我们取到局部数据以及拆分
reducers
,这时候
combineReducers
就派上用场了。
源码核心部分如下:
export default function combineReducers(reducers) {
// 各种异常处理和数据清洗
...
return function combination(state = {}, action) {
const finalReducers = {};
// 又是各种异常处理,finalReducers 是一个合法的 reducers map
...
let hasChanged = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[key];
// 获取前一次reducer
const previousStateForKey = state[key];
// 获取当前reducer
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
// 判断是否改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
// 如果没改变,返回前一个state,否则返回新的state
return hasChanged ? nextState : state;
}
}
注意这一句,每次都会拿新生成的 state
和前一次的对比,如果引用没变,就会返回之前的 state
,这也就是为什么值改变后reducer
要返回一个新对象的原因。
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
随着业务量的增大,我们就可以利用嵌套的 combineReducers
拼接我们的数据,但是就笔者的实践看来,大部分的业务数据都是深嵌套的简单数据操作,比如我要将state.booking.people.name
置为测试姓名,因此我们这边有一些别的解决思路,比如使用高阶 reducer
,又或者即根据path
来修改数据,举个例子:我们会 dispatch(update('booking.people.name','测试姓名'))
,然后在reducer
中根据 booking.people.name
这个 path
更改对应的数据。
compose
接受一组函数,会从右至左组合成一个新的函数,比如compose(f1,f2,f3)
就会生成这么一个函数:(...args) => f1(f2(f3(...args)))
核心就是这么一句
return funcs.reduce((a, b) => (...args) => a(b(...args)))
拿一个例子简单解析一下
[f1,f2,f3].reduce((a, b) => (...args) => a(b(...args)))
step1: 因为 reduce 没有默认值,reduce的第一个参数就是 f1,第二个参数是 f2,因此第一个循环返回的就是 (...args)=>f1(f2(...args)),这里我们先用compose1 来代表它
step2: 传入的第一个参数是前一次的返回值 compose1,第二个参数是 f3,可以得到此次的返回是 (...args)=>compose1(f3(...args)),即 (...args)=>f1(f2(f3(...args)))
bindActionCreator
简单说一下 actionCreator
是什么
一般我们会这么调用 action
dispatch({type:"Action",value:1})
但是为了保证 action
可以更好的复用,我们就会使用 actionCreator
function actionCreatorTest(value){
return {
type:"Action",
value
}
}
//调用时
dispatch(actionCreatorTest(1))
再进一步,我们每次调用 actionCreatorTest
时都需要使用 dispatch
,为了再简化这一步,就可以使用bindActionCreator
对 actionCreator
做一次封装,后续就可以直接调用封装后的函数,而不用显示的使用dispatch
了。
核心代码就是这么一段:
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
下面的代码主要是对 actionCreators
做一些操作,如果你传入的是一个 actionCreator
函数,会直接返回一个包装过后的函数,如果你传入的一个包含多个actionCreator
的对象,会对每个 actionCreator
都做一个封装。
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
//类型错误
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
...
)
}
// 处理多个actionCreators
var keys = Object.keys(actionCreators)
var boundActionCreators = {}
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
applyMiddleware
想一下这种场景,比如说你要对每次 dispatch(action)
都做一次日志记录,方便记录用户行为,又或者你在做某些操作前和操作后需要获取服务端的数据,这时可能需要对dispatch
或者 reducer
做一些封装,redux
应该是想好了这种用户场景,于是提供了middleware
的思路。
applyMiddleware
的代码也很精炼,具体代码如下:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
可以看到 applyMiddleware
内部先用 createStore
和 reducer
生成了store
,之后又用 store
生成了一个 middlewareAPI
,这里注意一下dispatch: (action) => dispatch(action)
,由于后续我们对 dispatch
做了修改,为了保证所有的middleware
中能拿到最新的 dispatch
,我们用了闭包对它进行了一次包裹。
之后我们执行了
chain = middlewares.map(middleware => middleware(middlewareAPI))
生成了一个 middleware
链 [m1,m2,...]
再往后就是 applyMiddleware
的核心,它将多个 middleWare
串联起来并依次执行
dispatch = compose(...chain)(store.dispatch)
compose
我们之前有讲过,这里其实就是 dispatch = m1(m2(dispatch))
。
最后,我们会用新生成的 dispatch
去覆盖 store
上的 dispatch
但是,在 middleware
内部究竟是如何实现的呢?我们可以结合 redux-thunk
的代码一起看看,redux-thunk
主要是为了执行异步操作,具体的API
和用法可以看 github,它的源码如下:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 用next而不是dispatch,保证可以进入下一个中间件
return next(action);
};
}
这里有三层函数
({ dispatch, getState })=>
这一层对应的就是前面的middleware(middlewareAPI)
next=>
对应前面compose
链的逻辑,再举个例子,m1(m2(dispatch))
,这里dispatch
是m2
的next
,m2(dispatch)
返回的函数是m1
的next
,这样就可以保证执行next
时可以进入下一个中间件action
这就是用户输入的action
到这里,整个中间件的逻辑就很清楚了,这里还有一个点要注意,就是在中间件的内部,dispatch
和 next
是要注意区分的,前面说到了,next
是为了进入下一个中间件,而由于之前提到的middlewareAPI
用到了闭包,如果在这里执行 dispatch
就会从最一开始的中间件重新再走一遍,如果middleWare
一直调用 dispatch
就可能导致无限循环。
那么这里的 dispatch
的目的是什么呢?就我看来,其实就是取决与你的中间件的分发思路。比如你在一个异步 action
中又调用了一个异步action
,此时你就希望再经过一遍 thunk middleware
,因此 thunk
中才会有action(dispatch, getState, extraArgument)
,将 dispatch
传回给调用方。
小结
结合这一段时间的学习,读了第二篇源码依然会有收获,比如它利用函数式和 curry
将代码做到了非常精简,又比如它的中间件的设计,又可以联想到AOP
和 express
的中间件。
那么,redux
是如何与 react
结合的?promise
,saga
又是如何实现的?与thunk
相比有和优劣呢?后面会继续阅读源码,记录笔记,如果有兴趣也可以 watch
我的 repo 等待后续更新。