redux简介(二)源码解析
写在开头
前置知识内容,闭包,高阶函数,函数式编程思想,redux核心概念。
-
redux文档:https://www.redux.org.cn
-
redux源码:https://github.com/reduxjs/redux
-
克隆代码
git clone https://github.com/reduxjs/redux.git
本文对应redux版本为redux v4.0.1
一、结构
redux源码结构在redux的src目录下可以清楚的看到redux的几个核心文件和js工具函数。通过阅读index.js
文件可以清楚的看到redux
导出的5个核心方法
文件 | 功能 | |
---|---|---|
index |
redux的入口文件 | 用法 |
createStore |
提供核心APIcreateStore 根据reducer,preState,applyMiddleware。创建并返回store |
|
combineReducers |
提供核心APIcombineReducers ,用于合并拆分的reducer |
|
bindActionCreators |
提供核心APIbindActionCreators 可以简化dispatch action的调用方法。 |
|
applyMiddleware |
提供核心APIapplyMiddleware 。 |
|
compose | ||
actionTypes.js |
内置的action.type。 | |
isPlainObject.js |
判断是否是简单对象。 | |
warning.js |
用于输出警告信息。 |
二、工具文件
2.1 actionTypes
源码截取
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.');
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
};
export default ActionTypes
actionTypes文件主要封装了redux内置的actionType,其中ActionTypes.INIT
主要用于初始化store所使用。REPLACE
PROBE_UNKNOWN_ACTION
为替换reducer的actionType。
2.2isPlainObject
源码截取
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
本段函数主要用于对 reducer函数的action参数进行校验。函数用于判断一个对象是否是一个简单对象,简单对象是指直接使用对象字面量{}
或者new Object()
、Object.create(null)
所创建的对象。jQuery和lodash等JavaScript函数均对此有所实现.redux的老期版本使用了ladash的实现版本,后改为自己实现。
2.3warning
源码截取:
export default function warning(message) {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message)
}
try {
// This error was thrown as a convenience so that if you enable
// "break on all exceptions" in your console,
// it would pause the execution at this line.
throw new Error(message)
} catch (e) {
}
}
本函数主要用于对代码执行过程中所遇到的错误进行统一处理,在控制台打印错误原因。使用throw new Error(message)是为了方便调试时中断执行
三、核心文件
3.1 createStore.js
使用场景
// reducer是必传的函数主要用于响应action对store做处理。
// preloadedState是一个可选对象,用于指定redux中store的默认值,使用场景比如说,服务端渲染是redux数据的注入,或者应用程序页面刷新时redux数据的保留。
// enhancer 用于增强redux的功能,比如处理异步,打印log等等
createStore(reducer, [preloadedState], enhancer)
creeateStore.js主要暴露了一个函数,即createStore函数。整个函数除去注释,约为180行左右。
下面将对此函数进行分析。此函数的参数为(rducer,preloadState,enhancer)
经过处理返回一个store对象,store对象的值如下。
{
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
createStore函数主要有5个变量用来存储信息,通过变量名,我们可以很容易的知道每个变量的含义。createStore通过闭包将这些变量存储起来,通过store的属性来操作数据。
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
源代码行数过多,在这里就不体现出来了,点击这里查看
下面来逐行分析createStore函数的执行过程
1.校验函数传入的参数
函数执行的第一步是函数校验,如果校验未通过,直接抛出错误。
函数的第一个参数是必传的函数(reducers),函数的额第二个参数是可选的除函数外的任意类型参数preloadState
,参数的第三个参数是可选的函数enhancer
(applyMiddleWare函数返回的函数组成的数组)。也可以传递两个参数reducers
和enhancer
。
函数校验了四个地方
第一步,由于函数可以支持多个中间件函数,但是多个中间件必须要通过compose
函数包装之后在可以传入。函数首先对此进行了校验。判断如果函数的第二个和第三个参数都为函数。或者函数的第三个参数和第四个参数都为函数。则可以猜测到,开发者可能是为使用compose包装多个中间件,所导致,所以此时函数会抛出错误信息。提示出当前可能的错误原因。
第二步,对于函数只传入reducers
和enhancer
进行了处理。判断传入的第二个参数是函数,并且没有传递第三个参数,此时将preloadState赋值给enhancer。将preloadState置空。是实参与形参相对应。
第三步,判断enhancer是否存在,如果不是函数,就抛出错误。如果是函数则直接返回enhancer(createStore)(reducer, preloadedState)
,这里可能不好理解,需要参照applyMiddleWare函数部分源码。
第四步,判断传入的reducer参数的类型是否是函数,如果不是函数,抛出错误。
到这里,函数参数的校验就结束了。
2.定义函数内的局部变量来存储传入的参数和后续会用到的状态等信息
let currentReducer = reducer //传入的reducer参数
let currentState = preloadedState //当前store里的state
let currentListeners = []//当前监听函数数组
let nextListeners = currentListeners //接下来的监听函数数组
let isDispatching = false //是否正在dispatch
3.定义六个函数来对来处理store的数据交互
第一个函数:ensureCanMutateNextListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
此函数的功能较为简单,就不在说明
第二个函数:getState
function getState() {
if (isDispatching) {
throw new Error('msg')
}
return currentState
}
此函数就是最终暴露出来的store.dispatch
函数,函数首先判断当前是不是处于dispatching状态。是的话直接抛出错误。否则就将函数的局部变量current返回。
第三个函数:subscribe
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('msg')
}
if (isDispatching) {
throw new Error('msg')
}
let isSubscribed = true;
ensureCanMutateNextListeners();
nextListeners.push(listener);
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error('msg')
}
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1)
}
}
此函数也是最终暴露出来的store.subscribe
函数,此函数用于添加一个订阅state
变化的函数。传入参数为函数,如果传入非函数,或者正在dispatching时调用,会抛出错误。函数返回了一个函数用于取消订阅。
接下来逐步分析此函数的执行过程,首先参数和当前状态的校验。然后定义局部变量标记是否已经订阅并标记为true,然后调用之前的ensureCanMutateNextListeners()
函数。以确保nextListeners
与currentListeners相同。然后将传入的listener函数添加到nextListeners
数组。随后返回一个新的函数用于取消订阅。
在返回的新的函数中,首先做了状态的判断,如果正在dispatching,那么抛出错误,通过之前定义的是否订阅标记判断,当前的listener是否还在被订阅。如果已经取消了订阅,那么直接返回,(这里主要处理了取消订阅函数可以被多次调用从而产生错误的情况。这里使用了闭包)否则,将取消订阅的标记置为false。再此执行ensureCanMutateNextListeners()
,原因同上。然后使用数组的indexOf方法在当前的
nextListeners
数组中找出对应的索引。使用数组的splice
方法将其删除。
第四个函数:dispatch
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error('msg' )
}
if (typeof action.type === 'undefined') {
throw new Error('msg')
}
if (isDispatching) {
throw new Error('msg')
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener()
}
return action
}
这个函数也是最终暴露出来的store.dispatch
方法。主要用于触发action以修改state,是非常重要的几个函数之一。函数执行过程中。首先校验参数action必须为普通javaScript
对象,且action.type必须不能为undefined,并且当前不能处于dispatching状态。否则就会抛出错误。校验通过后。将isDispatching置为true,通过执行currentState = currentReducer(currentState, action)
更改state,此时便成功的触发了一个action。在执行完成之后,将isDispatching
置为false。随后依次执行订阅此store的函数。代码如下
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener()
}
最后将action参数返回
第五个函数:replaceReducer
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer;
dispatch({ type: ActionTypes.REPLACE })
}
此函数也是直接暴露出来的store.replaceReducer
函数,一般在实际开发中使用较少。 主要用于动态加载reducer
。
第六个函数:observable
function observable() {
const outerSubscribe = subscribe;
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState();
const unsubscribe = outerSubscribe(observeState);
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
接下来createStore
函数将直接返回已经创建好的函数。
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
3.2 combineReducers.js
combineReducers.js
文件在去除注释和错误提示的情况下,代码行数约为130行左右。查看源码。此js文件中主要有4个函数
函数名 | 作用 | 备注 |
---|---|---|
getUndefinedStateErrorMessage |
供redux内部调用,根绝action和key生成错误信息,例如reduce函数返回undefined时的情况 | |
getUnexpectedStateShapeWarningMessage |
供redux内部调用,用于获取警告信息,主要进行了 reducer的判空,当前的state是否为简单对象,给state中存在而reducer中不存在的属性添加缓存标识,并返回警告信息。 | |
assertReducerShape |
检测用于组合的reducer是否符合redux对顶的reducer | |
combineReducers |
直接暴露出来,用于合并reducer |
assertReducerShape,主要对于传入的reducer数组进行便利进行检验,首先调用reducer(undefined,{ type: ActionTypes.INIT })
以获取initState
,initState
如果是undefined
则抛出错误信息,提示必须返回初始state,如果不想为这个reducer设置值,要返回null而不是undefined。如果没有出错,则调用reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() })
,通过未知的action,来检测reducer能否正确处理,即返回的值类型是否为undefined
,如果是则抛出错误原因。
combineReducers, 函数首先对传入的reducers对象进行遍历,将结果赋值到局部变量finalReducers
。如果不是正式环境,那么对于为null的reducer进行提示,如果是正式环境,则忽略类型部位函数的reducer。然后调用assertReducerShape对finalReducers
进行类型校验,并存储错误信息到局部变量shapeAssertionError。然后反对一个合并完成的reducer函数。
在新返回的函数中,首先判断shapeAssertionError,如果存在错误就抛出,对于非正式环境,使用getUnexpectedStateShapeWarningMessage
进行校验,并提醒错误。定义一个标记表示state是否已经变化并置为false,定义下一个状态的结果nextState
,接下来遍历执行reducer。最终将nextState返回
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
3.3 applyMiddleware.js
源码截取
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
// 注意由于函数说引用类型所以此时middlewareAPI的dispatch函数也一同更改。
return {
...store,
dispatch
}
}
}
applyMiddleware函数主要是与中间件有关的函数,他允许我们在action到达reducer之前对action进行加工处理。
使用
// createStore中对于中间件的调用
return enhancer(createStore)(reducer, preloadedState)
//applyMiddleware函数的使用;
let store=createStore(reducer,preloadStste,applyMiddleware(thunk))
// redux-thunk源码
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
这里部分源码设计到了高阶函数,初次阅读可能不太好理解,所以将于applyMiddleware有关的内容均列在了上面。由此来梳理applyMiddleware函数的工作流程。
在创建store时,首先调用createStore方法,第三个参数为applyMiddleware
函数返回的新的函数(起名字为函数1,源码中第一个箭头函数)。在createStore
源码中可以看到,检测到第三个参数数为函数时将会执行此函数(函数1),并且把createStore
函数作为函数1的参数。函数1的执行会返回一个新的函数(函数2,源码中第二个箭头函数)。通过createStore
源码可以看到,此时再次对函数二,进行了执行,传入的参数时reducer和preloadState。此时便开始执行applyMiddleware函数的主体部分。
在applyMiddleware中可以看到,首先根据传入的reducer和preloadState创建了store。然后定义了一个dispatch函数。作用是执行时的校验,避免在执行函数时dispaching
。然后创建了middlewareAPI
对象供中间件函数使用。然后将middlewareAPI
作为参数,便利执行中间件数组函数。将返回的结果存储在chain数组。通过thunk函数的源码使得我们可以了解到此时存储在chain书中的的每一项仍然是一个函数,函数接收的形参为next。随后将store.dispatch作为参数执行chain数组中的每一个函数,具体为首先执行第一个函数,将store.dispatch作为实参传入。将其返回的结果作为第二个函数的参数,以此类推,将最后结果赋值给dispatch。根绝thunk的源码可以看到,对于chain数组的每一项,执行后仍然会返回一个新的函数,这个新的函数的形参为action,这个新的函数恰恰就是我们要自己开发的中间件函数。最后将store和dispatch返回。此时回到createStore
源码,可以看到,返回的恰恰就是最终创建的store。
现在已经梳理创建含有中间件的store的函数执行过程。下面来分析一下store.dispatch
这个过程。
接着上面的来说,最终返回了dispatch函数并且作为了store的一个属性。当我们触发action的时候,显然使用的就是这个dispatch函数。现在我们来看看这个dispatch函数是什么样子的。
由于之前使用了compose函数,所以这部分呢可能不太容易理解,此时我们,假设两个中间件函数第一个为上面所提到的thunk
中间件,假设第二个为打印log的中间件,经过map处理后的如下。
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
为了利于理解我们首先考虑只有一个中间件的情况,此时
dispatch=componse([logger(middlewareAPI)])(store.dispatch)
//等同于
dispatch=logger(middlewareAPI)(store.dispatch)
// 所以
store.dispatch(action)=logger(middlewareAPI)(store.dispatch)(action)
// 此时发现
logger(middlewareAPI)(store.dispatch)(action) 与定义时的刚好对应
考虑有多个中间件的情况。
dispatch=componse([logger(middlewareAPI),thunk(middlewareAPI)])(store.dispatch)
执行流程分析
- 根据compose函数的源码可以将上面表达式转化为
dispatch=thunk(middlewareAPI)(logger((middlewareAPI)(store.dispatch)))
- 首先执行logger(middlewareAPI)(store.dispatch)函数,函数返回一个类似于next=>action=>{} 的函数。执行这个函数,传入的参数next的值恰好是store,dispatch。
- 然后执行thunk函数,函数也返回一个类似于next=>action=>{} 的函数。thunk()函数执行时传入的是logger函数返回的函数。
- 多个中间件以此类似
- 当到最后一个中间件时,返回类似于next=>action=>{}的函数。next参数是倒数第二个中间件返回的函数。最后一个函数返回的函数被赋值给了dispatch
总结:通过闭包存储了最新的store值。通过compose函数,使得每个中间件的next参数指向其后面的中间件函数。最后一个中间件指向store.dispatch。当触发action时,action会依次的经过中间件的处理。在每个中间件中可以通过store.getState()取得最新的state值,通过dispatch可以从第一个中间件触发dispatch()。通过调用next(action)触发下一个中间件函数
dispatch = compose(...chain)(store.dispatch)
compose函数
函数传入的数组的每一项是形如next=>action=>{}的函数。
作用
- 每一个函数的next参数是对他之后函数的返回值
- 最后一个函数的action是store.dispatch
- 最后的返回值是第一个函数的返回值,赋值给dispatch
- 每次调用dispatch,就是调用第一个中间件
- 在中间件函数内调用next就是调用下一个中间件的
(action)=>{}
- 调用最后一个中间件的next,会调用store.dispatch,更新state。
接着从applyMiddleware
函数源码compose
部分开始分析。此时的dispatch函数如下
dispatch=componse([chainOne,chainTwo])(store.dispatch)
根据compose源码。
dispatch=chainTwo(chainOne(store.dispatch))
3.4 bindActionCreators.js
源码截取
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 校验参数必须为对象
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${
actionCreators === null ? 'null' : typeof actionCreators
}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
const boundActionCreators = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
在redux中改变state只能通过store.dispatch(action)
的方式,这样迫使我们不得不将store.dispatch
函数进行逐层传递,这样会增加一些无用的重复代码。
这时就需要使用bindActionCreators
来加工actionCreators
函数。bindActionCreators(actionCreators, dispatch)
返回一个与原函数/对象相同的新函数。因为过闭包保存了store.dispatch
并且通过apply
调整了this的指向。直接执行返回的函数/对象的属性便可以触发数据的改变,使得我们不在需要将dispatch逐层传递,也使得我们可以像执行普通函数一样来触发action。
3.5 compose.js
源码截取
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)))
}
函数的作用为将多个函数组合成一个函数。
使得compose(f,g,h)(...arg)
等同于(...arg)=>f(g(h(...arg)))
在源码的applyMiddleware函数中使用了此函数。
四、总结
以上这些内容就是我对于redux的部分理解。
redux简单的说就是一个状态管理工具。也可以与除了react之外的其他框架组合使用,比如说Vue.js
,当然选择Vuex对于Vue是更好的选择。对于React来说redux也不是其唯一的状态管理工具,除此之外也有dva,mobox
五、写在最后
以上这些内容就是我对于redux的部分理解。从开始学习至文章产出大约一周左右。由于个人能力有限,所以可能有很多的不足之处。当然这篇文章也会不断的更新,完善。
预告下一篇不定时更新文章,redux有关部分框架的源码解析,比如react-redux
,react-saga
,reacr-router-redux
。
推荐下载源码进行阅读学习,相信会有很多的收获,加油