前言
上篇文章《我的源码阅读之路:redux源码剖析(上)》由于知乎对文章篇幅的限制,所以上篇文章只讲到了createStore.js。这篇文章将接着上篇文章未剖析的redux源码进行剖析。
combineReducers.js
这个js对应着redux里的combineReducers方法,主要作用就是合并多个reducer。现在我们先给一个空的函数,然后再一步步地根据还原源码,这样大家可能理解得更为透彻点。
//reducers Object类型 每个属性对应的值都要是function
export default function combineReducers(reducers) {
....
}
第一步:浅拷贝reducers
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
}
这里定义了一个finalReducers和finalReducerKeys,分别用来拷贝reducers和其属性。先用Object.keys方法拿到reducers所有的属性,然后进行for循环,每一项可根据其属性拿到对应的reducer,并浅拷贝到finalReducers中,但是前提条件是每个reducer的类型必须是Function,不然会直接跳过不拷贝。
第二步:检测finalReducers里的每个reducer是否都有默认返回值
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
const initialState = reducer(undefined, { type: ActionTypes.INIT })
if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined. If you don't want to set a value for this reducer, ` +
`you can use null instead of undefined.`
)
}
const type =
'@@redux/PROBE_UNKNOWN_ACTION_' +
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
if (typeof reducer(undefined, { type }) === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${
ActionTypes.INIT
} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined, but can be null.`
)
}
})
}
export default function combineReducers(reducers) {
//省略第一步的代码
......
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
}
assertReducerShape方法主要检测两点: - 不能占用的命名空间 - 如果遇到未知的action的类型,不需要要用默认返回值
如果传入type为 @@redux/INIT<随机值> 的action,返回undefined,说明没有对未 知的action的类型做响应,需要加默认值。如果对应type为 @@redux/INIT<随机值> 的action返回不为undefined,但是却对应type为 @@redux/PROBE_UNKNOWN_ACTION_<随机值> 返回为undefined,说明占用了 命名空间。整个逻辑相对简单,好好自己梳理一下。
第三步:返回一个函数,用于代理所有的reducer
export default function combineReducers(reducers) {
//省略第一步和第二步的代码
......
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
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
}
}
首先对传入的state用getUnexpectedStateShapeWarningMessage做了一个异常检测,找出state里面没有对应reducer的key,并提示开发者做调整。接着我们跳到getUnexpectedStateShapeWarningMessage里,看其实现。
function getUnexpectedStateShapeWarningMessage(
inputState,
reducers,
action,
unexpectedKeyCache
) {
const reducerKeys = Object.keys(reducers)
const argumentName =
action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer'
if (reducerKeys.length === 0) {
return (
'Store does not have a valid reducer. Make sure the argument passed ' +
'to combineReducers is an object whose values are reducers.'
)
}
if (!isPlainObject(inputState)) {
return (
`The ${argumentName} has unexpected type of "` +
{}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
`". Expected argument to be an object with the following ` +
`keys: "${reducerKeys.join('", "')}"`
)
}
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})
if (action && action.type === ActionTypes.REPLACE) return
if (unexpectedKeys.length > 0) {
return (
`Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
`"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
`Expected to find one of the known reducer keys instead: ` +
`"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
)
}
}
getUnexpectedStateShapeWarningMessage接收四个参数 inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),这里要说一下unexpectedKeyCache是上一次检测inputState得到的其里面没有对应的reducer集合里的异常key的集合。整个逻辑如下:
- 前置条件判断,保证reducers集合不为{}以及inputState为简单对象
- 找出inputState里有的key但是 reducers集合里没有key
- 如果是替换reducer的action,跳过第四步,不打印异常信息
- 将所有异常的key打印出来
getUnexpectedStateShapeWarningMessage分析完之后,我们接着看后面的代码。
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
首先定义了一个hasChanged变量用来表示state是否发生变化,遍历reducers集合,将每个reducer对应的原state传入其中,得出其对应的新的state。紧接着后面对新的state做了一层未定义的校验,函数getUndefinedStateErrorMessage的代码如下:
function getUndefinedStateErrorMessage(key, action) {
const actionType = action && action.type
const actionDescription =
(actionType && `action "${String(actionType)}"`) || 'an action'
return (
`Given ${actionDescription}, reducer "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
逻辑很简单,仅仅做了一下错误信息的拼接。未定义校验完了之后,会跟原state作对比,得出其是否发生变化。最后发生变化返回nextState,否则返回state。
compose.js
这个函数主要作用就是将多个函数连接起来,将一个函数的返回值作为另一个函数的传参进行计算,得出最终的返回值。以烹饪为例,每到料理都是从最初的食材经过一道又一道的工序处理才得到的。compose的用处就可以将这些烹饪工序连接到一起,你只需要提供食材,它会自动帮你经过一道又一道的工序处理,烹饪出这道料理。
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)))
}
上面是es6的代码,可能小伙伴们并不是很好理解,为了方便大家理解,我将其转换成es5代码去做讲解。
function compose() {
var _len = arguments.length;
var funcs = [];
for (var i = 0; i < _len; i++) {
funcs[i] = arguments[i];
}
if (funcs.length === 0) {
return function (arg) {
return arg;
};
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}
梳理一下整个流程,大致分为这么几步:
- 新建一个新数组funcs,将arguments里面的每一项一一拷贝到funcs中去
- 当funcs的长度为0时,返回一个传入什么就返回什么的函数
- 当funcs的长度为1时,返回funcs第0项对应的函数
- 当funcs的长度大于1时,调用Array.prototype.reduce方法进行整合
这里我们正好复习一下数组的reduce方法,函数reduce接受下面四个参数 - total 初始值或者计算得出的返回值 - current 当前元素 - index 当前元素的下标 - array 当前元素所在的数组
示例:
const array = [1,2,3,4,5,6,7,8,9,10];
const totalValue=array.reduce((total,current)=>{
return total+current
}); //55
这里的compose有个特点,他不是从左到右执行的,而是从右到左执行的,下面我们看个例子:
const value=compose(function(value){
return value+1;
},function(value){
return value*2;
},function(value){
return value-3;
})(2);
console.log(value);//(2-3)*2+1=-1
如果想要其从左向右执行也很简单,做一下顺序的颠倒即可。
===> 转换前 return a(b.apply(undefined, arguments));
===> 转换后 return b(a.apply(undefined, arguments));
applyMiddleware.js
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)
return {
...store,
dispatch
}
}
}
前面我们讲enhancer的时候,提到过这个applyMiddleware,现在我们将二者的格式对比看一下。
// enhancer
function enhancer(createStore) {
return (reducer,preloadedState) => {
//逻辑代码
.......
}
}
//applyMiddleware
function //applyMiddleware(...middlewares) {
return createStore => (...args) => {
//逻辑代码
.......
}
}
通过二者的对比,我们发现函数applyMiddleware的返回就是一个enhancer,下面我们再看其具体实现逻辑:
- 通过createStore方法创建出一个store
- 定一个dispatch,如果在中间件构造过程中调用,抛出错误提示
- 定义middlewareAPI,有两个方法,一个是getState,另一个是dispatch,将其作为中间件调用的store的桥接
- middlewares调用Array.prototype.map进行改造,存放在chain
- 用compose整合chain数组,并赋值给dispatch
- 将新的dispatch替换原先的store.dispatch
看完整个过程可能小伙伴们还是一头雾水,玄学的很!不过没关系,我们以redux-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;
哈哈哈!看完redux-thunk的源码之后是不是很奔溃,几千star的项目居然就几行代码,顿时三观就毁了有木有?其实源码没有大家想象的那么复杂,不要一听源码就慌。稳住!我们能赢!根据redux-thunk的源码,我们拿到的thunk应该是这样子的:
const thunk = ({ dispatch, getState })=>{
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
我们经过applyMiddleware处理一下,到第四步的时候,chain数组应该是这样子的:
const newDispatch;
const middlewareAPI={
getState:store.getState,
dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const fun1 = (next)=>{
return action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
}
const chain = [fun1]
compose整合完chain数组之后得到的新的dispatch的应该是这样子:
const newDispatch;
const middlewareAPI={
getState:store.getState,
dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const next = store.dispatch;
newDispatch = action =>{
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
接下来我们可以结合redux-thunk的例子来模拟整个过程:
function makeASandwichWithSecretSauce(forPerson) {
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
// store.dispatch就等价于newDispatch
store.dispatch(makeASandwichWithSecretSauce('Me'))
====> 转换
const forPerson = 'Me';
const action = (dispatch)=>{
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
}
newDispatch()
===> typeof action === 'function' 成立时
((dispatch)=>{
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
})( (...args) => newDispatch(...args), getState)
====> 计算运行结果
const forPerson = 'Me';
const dispatch = (...args) => newDispatch(...args) ;
fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
// 其中:
function fetchSecretSauce() {
return fetch('https://www.google.com/search?q=secret+sauce');
}
function makeASandwich(forPerson, secretSauce) {
return {
type: 'MAKE_SANDWICH',
forPerson,
secretSauce
};
}
function apologize(fromPerson, toPerson, error) {
return {
type: 'APOLOGIZE',
fromPerson,
toPerson,
error
};
}
====> 我们这里只计算Promise.resolve的结果,并且假设fetchSecretSauce返回值为'666',即sauce='666'
const forPerson = 'Me';
const dispatch = (...args) => newDispatch(...args) ;
dispatch({
type: 'MAKE_SANDWICH',
'Me',
'666'
})
====> 为了方便对比,我们再次转换一下
const action = {
type: 'MAKE_SANDWICH',
'Me',
'666'
};
const next = store.dispatch
const newDispatch = action =>{
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
newDispatch(action)
====> 最终结果
store.dispatch({
type: 'MAKE_SANDWICH',
'Me',
'666'
});
以上就是redux-thunk整个流程,第一次看肯能依旧会很懵,后面可以走一遍,推导一下加深自己的理解。
bindActionCreators.js
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 keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
bindActionCreators针对于三种情况有三种返回值,下面我们根据每种情况的返回值去分析。(为了方便理解,我们选择在无集成中间件的情况)
typeof actionCreators === 'function'
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
const actionFun=bindActionCreator(actionCreators, dispatch)
===> 整合一下
const fun1 = actionCreators;
const dispatch= stror.dispatch;
const actionFun=function () {
return dispatch(fun1.apply(this, arguments))
}
根据上面的推导,当变量actionCreators的类型为Function时,actionCreators必须返回一个action。
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"?`
)
提示开发者actionCreators类型错误,应该是一个非空对象或者是函数。
默认
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
通过和第一种情况对比发现,当actionCreators的每一项都执行一次第一种情况的操作。换句话说,默认情况是第一种情况的集合。
以上是对bindActionCreators的剖析,可能小伙伴们对这个还是不够理解,不过没有关系,只要知道bindActionCreators干了啥就行。bindActionCreators是需要结合react-redux一起使用的,由于本篇文章没有讲解react-redux,所以这里我们不对bindActionCreators做更深入的讲解。下篇文章讲react-redux,会再次提到bindActionCreators。
结语
到这里整个redux的源码我们已经剖析完了,整个redux代码量不是很大,但是里面的东西还是很多的,逻辑相对来说有点绕。不过没关系,没有什么是看了好几次都看不懂的,如果有那就再多看几次嘛!另外再多一嘴,如果想快读提高自己的小伙伴们,我个人是强烈推荐看源码的。正所谓“近朱者赤,近墨者黑”,多看看大神的代码,对自己的代码书写、代码逻辑、知识点查缺补漏等等方面都是很大帮助的。就拿我自己来说,我每次阅读完一篇源码之后,都受益匪浅。可能第一次看源码,有着诸多的不适应,毕竟万事开头难,如果强迫自己完成第一次的源码阅读,那往后的源码阅读将会越来越轻松,对自己的提升也就越来越快。各位骚年们,撸起袖子加油干吧!