我的源码阅读之路:redux源码剖析(下)

872 阅读6分钟
原文链接: zhuanlan.zhihu.com

前言

上篇文章《我的源码阅读之路: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的集合。整个逻辑如下:

  1. 前置条件判断,保证reducers集合不为{}以及inputState为简单对象
  2. 找出inputState里有的key但是 reducers集合里没有key
  3. 如果是替换reducer的action,跳过第四步,不打印异常信息
  4. 将所有异常的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));
    };
  });
}

梳理一下整个流程,大致分为这么几步:

  1. 新建一个新数组funcs,将arguments里面的每一项一一拷贝到funcs中去
  2. 当funcs的长度为0时,返回一个传入什么就返回什么的函数
  3. 当funcs的长度为1时,返回funcs第0项对应的函数
  4. 当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,下面我们再看其具体实现逻辑:

  1. 通过createStore方法创建出一个store
  2. 定一个dispatch,如果在中间件构造过程中调用,抛出错误提示
  3. 定义middlewareAPI,有两个方法,一个是getState,另一个是dispatch,将其作为中间件调用的store的桥接
  4. middlewares调用Array.prototype.map进行改造,存放在chain
  5. 用compose整合chain数组,并赋值给dispatch
  6. 将新的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代码量不是很大,但是里面的东西还是很多的,逻辑相对来说有点绕。不过没关系,没有什么是看了好几次都看不懂的,如果有那就再多看几次嘛!另外再多一嘴,如果想快读提高自己的小伙伴们,我个人是强烈推荐看源码的。正所谓“近朱者赤,近墨者黑”,多看看大神的代码,对自己的代码书写、代码逻辑、知识点查缺补漏等等方面都是很大帮助的。就拿我自己来说,我每次阅读完一篇源码之后,都受益匪浅。可能第一次看源码,有着诸多的不适应,毕竟万事开头难,如果强迫自己完成第一次的源码阅读,那往后的源码阅读将会越来越轻松,对自己的提升也就越来越快。各位骚年们,撸起袖子加油干吧!