同步自我的 博客
很久之前就看过一遍 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(...)
)}
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 等待后续更新。