Redux 学习笔记 - 源码阅读
2018-02-05 10:17
447 查看
很久之前就看过一遍
注意:本文不是单纯的讲
用户触发
代码量也不大,源码结构很简单:
其中
这里有这么一段代码,主要是为了校验非生产环境下是否使用的是未压缩的代码,压缩之后,因为函数名会变化,
// 初始化的 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,
4000
replaceReducer,
[$$observable]: observable
}
}
下面我们具体来说
这里我们看到了
这时候的输出就会是
在后续的
它的目的则是确保每次
这里有两点说一下我的看法:
为什么
在循环执行
乍一看觉得会为什么不直接
我先贴一个正常的
源码核心部分如下:
注意这一句,每次都会拿新生成的
随着业务量的增大,我们就可以利用嵌套的
核心就是这么一句
拿一个例子简单解析一下
一般我们会这么调用
但是为了保证
再进一步,我们每次调用
核心代码就是这么一段:
下面的代码主要是对
可以看到
之后我们执行了
生成了一个
再往后就是
最后,我们会用新生成的
但是,在
github,它的源码如下:
这里有三层函数
到这里,整个中间件的逻辑就很清楚了,这里还有一个点要注意,就是在中间件的内部,
那么这里的
那么,
abb4
是如何与
repo 等待后续更新。
Redux相关技术栈的源码,最近在看书的时候发现有些细节已经忘了,而且发现当时的理解有些偏差,打算写几篇学习笔记。这是第一篇,主要记录一下我对
Redux
、redux-thunk源码的理解。我会讲一下大体的架构,和一些核心部分的代码解释,更具体的代码解释可以去看我的repo,后续会继续更新
react-redux,以及一些别的
redux中间件的代码和学习笔记。
注意:本文不是单纯的讲
API,如果不了解的可以先看一下文档,或者
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,
4000
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
abb4
是如何与
react结合的?
promise,
saga又是如何实现的?与
thunk相比有和优劣呢?后面会继续阅读源码,记录笔记,如果有兴趣也可以
watch我的
repo 等待后续更新。
相关文章推荐
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Redux 学习笔记 - 源码阅读
- Halide学习笔记----Halide tutorial源码阅读19
- Java框架类源码阅读学习笔记
- 阅读{django-restframework}源码[generics.py]学习笔记
- obs-studio源码阅读笔记:学习obs流程的第一步,test程序
- Halide学习笔记----Halide tutorial源码阅读8
- api.js源码阅读学习笔记
- Halide学习笔记----Halide tutorial源码阅读6
- Halide学习笔记----Halide tutorial源码阅读16
- gtk学习笔记 - vim + ctags 阅读源码
- Halide学习笔记----Halide tutorial源码阅读4
- Halide学习笔记----Halide tutorial源码阅读13
- Halide学习笔记----Halide tutorial源码阅读5
- Halide学习笔记----Halide tutorial源码阅读14