同步自个人 博客react
好久以前就看过一遍 Redux
相关技术栈的源码,最近在看书的时候发现有些细节已经忘了,并且发现当时的理解有些误差,打算写几篇学习笔记。这是第一篇,主要记录一下我对 Redux
、redux-thunk
源码的理解。我会讲一下大致的架构,和一些核心部分的代码解释,更具体的代码解释能够去看个人 repo,后续会继续更新 react-redux
,以及一些别的 redux
中间件的代码和学习笔记。git
注意:本文不是单纯的讲 API
,若是不了解的能够先看一下文档,或者 google
一下 Redux
相关的基础内容。github
在我看来,Redux 核心理念很简单express
store
负责存储数据redux
用户触发 action
数组
reducer
监听 action
变化,更新数据,生成新的 store
promise
代码量也不大,源码结构很简单:闭包
.src |- utils |- applyMiddleware.js |- bindActionCreators.js |- combineReducers.js |- compose.js |- createStore.js |- index.js
其中 utils
只包含一个 warning
相关的函数,这里就不说了,具体讲讲别的几个函数架构
这是入口函数,主要是为了暴露 Redux
的 API
app
这里有这么一段代码,主要是为了校验非生产环境下是否使用的是未压缩的代码,压缩以后,由于函数名会变化,isCrushed.name
就不等于 isCrushed
if ( process.env.NODE_ENV !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed' ) { warning(...) )}
这个函数是 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
主要是声明了一个默认的 action
,用于 reducer
的初始化。
它的目的主要是保存一份快照,下面咱们就讲讲 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
的内部实现很是简单,就是将当前的 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
。
获取当前的 state
,代码很简单,就不贴了。
更换当前的 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
或者 reactive
库提供的 API
,Reux
内部并无使用这个 API
,暂时不解释了。
先问个问题:为何要提供一个 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(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)))
简单说一下 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 }
想一下这种场景,好比说你要对每次 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 等待后续更新。