Life of xhu

About

Redux源码笔记(2)

Jan 17, 2016

  |   #Redux

首先我们先看一下utils目录下的几个工具函数。

src/utils/pick.js

export default function pick(obj, fn) {
  return Object.keys(obj).reduce((result, key) => {
    if (fn(obj[key])) {
      result[key] = obj[key]
    }
    return result
  }, {})
}

这个函数很像Array.prototype#filter,不过它是为对象准备的,作用是给每一个value作筛选,如果value没有通过校验,就舍弃响应的key,最后返回新生成的对象。

src/utils/mapValues.js

export default function mapValues(obj, fn) {
  return Object.keys(obj).reduce((result, key) => {
    result[key] = fn(obj[key], key)
    return result
  }, {})
}

这个函数也是对一个对象做遍历,然后把value以及key传入参数中的函数fn里,并把结果作为value,和当前key存到一个新对象中,最后返回这个对象。


src/combineReducers.js

这个文件一开始定义了几种错误情况的Error/Warning Message.

function getUndefinedStateErrorMessage(key, action) {
  var actionType = action && action.type
  var actionName = actionType && `"${actionType.toString()}"` || 'an action'

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  )
}

当一个reducer对于一个action没有返回state的时候,会抛出这个错误,所以在reducer的switch语句里,用于处理未知type的default条件应该返回当前state。


function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) {
  var reducerKeys = Object.keys(reducers)
  var argumentName = action && action.type === ActionTypes.INIT ?
    'initialState 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('", "')}"`
    )
  }

  var unexpectedKeys = Object.keys(inputState).filter(key => !reducers.hasOwnProperty(key))

  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.`
    )
  }
}

这里是每次reducer操作state时进行的一些校验,并输出响应的message,进行的校验有:

  1. 检测store中reducer的数量,如果没有reducer的话抛出错误信息;
  2. 作为reducer参数的state必须是朴素对象,否则会抛出错误信息;
  3. state中的key应该全都也全都有相应的reducer,否则抛出错误信息。

function assertReducerSanity(reducers) {
  Object.keys(reducers).forEach(key => {
    var reducer = reducers[key]
    var 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.`
      )
    }

    var 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.`
      )
    }
  })
}

这段代码主要是校验reducer是否进行了异常处理,也就是说,对于参数中的action的type字段,如果是系统保留的@@redux/INIT或者未知常量,那么reducer应该返回当前state,而不应该出现undefined的情况。


export default function combineReducers(reducers) {
  var finalReducers = pick(reducers, (val) => typeof val === 'function')
  var sanityError

  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

暴露一个combineReducers函数,接收一个reducer数组,并且使用上文中的pick函数对数组内容进行校验,也就是reducer的类型必须是一个函数,并且把校验过的对象保存为finalReducers


  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
      if (warningMessage) {
        console.error(warningMessage)
      }
    }

combineReducers方法返回一个combination函数, 这个函数才是reducer处理state的关键,在这个函数的开始,会判断reducer是否有校验错误,有的话抛出,然后进行第二段代码中的校验,并且打印出错误信息。


    var hasChanged = false
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
      return nextStateForKey
    })

    return hasChanged ? finalState : state
  }
}

这个部分的主体就是上面说的mapValues函数,被传入函数fn的参数是finalReducers对象中的键(reducer函数名)和值(reducer函数内容),然后从state中以reducer函数名作为key获得这个reducer应该处理的state即previousStateForKey,并且把state和action作为参数执行reducer函数,获得处理后的state即nextStateForKey。然后根据处理前和处理后的state进行对比来判断store里的内容是否进行了改变,再把nextStateForKey作为value存入结果对象finalState中。最后根据是否有state改变来选择返回原有state还是处理后的finalState

这里需要注意的有几点:

  1. Redux并不知道store里state的哪个分支该由哪个reducer来处理,而是对每一个reducer都用相同的action执行一遍,所以在一个项目里的action最好不要有重名的type,否则每次两个reducer都会同时执行。
  2. 正因为上一点的存在,Redux里dispatch一个action的时候,其实每个reducer都会响应这个action,所以一个reducer对于未知的type一定要有异常处理,即返回当前state,返回undefined会抛异常。
  3. state应该是immutable的,reducer对于state的任何操作都应该生成一个新对象,而不是在旧的对象上修改,这样Redux就可以简单的通过nextStateForKey !== previousStateForKey来判断state是否有改变。

到这里,combineReducers.js这个文件的讲解也就结束了,Redux源码中代码量最大的两个文件就这样搞定了,可以看出其实Redux的代码量是很小的,而且用了一些约定(比如immutable state)来让程序更加简洁优美。

这里还有一个小tip,就是在JavaScript中,当用花括号来包裹基本变量时,会自动以字面量的形式创建一个对象,combineReducers函数就是这样创建以reducer函数名为key函数内容为value的对象的。比如:

> a = 'test'
'test'
> b = (test) => console.log(test)
[Function]
> c = {a, b}
{ a: 'test', b: [Function] }