最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入Redux源码(二)combineReducers

    正文概述 掘金(ZebWu)   2020-12-12   441

    随着 Web 应用变得越来越复杂,你需要将 redux 状态进行拆分,每个模块使用独立的 reducer。你一定离不开 combineReducers 。

    阅读源码之前

    和往常一样,在阅读源码之前,回顾一下官方文档对该 API 的描述。

    combineReducers 是一个 helper function,接收多个 reducing function,返回一个 resulting reducer。resulting reducer 执行的时候会调用所有的 child reducer,并且将它们的结果以一个对象返回,并给每个 state 确定命名空间。命名空间取决于传递给 combineReducers 的对象的 key

    combineReducers 只有一个参数——reducers 组成的对象。

    combineReducers 实现了一些检查规则,以减少开发中遇到的问题:

    1. 如果遇到预期之外的 ActionType,你应该将 state 原样返回。

    2. 永远不要 return undefinedcombineReducers 会在这种情况下抛出错误。

    3. 如果 reducers 接收的 stateundefined ,你应该返回 initial state

    另外,combineReducers 会对你的 reducer 函数进行检查(传 undefined 给它们),即使你定义了 initial state

    同样,我们要带着问题去阅读源码。从上文的描述,我产生了以下问题:

    1. combineReducers 的 namespace 是如何实现的?
    2. combineReducers 的检查是如何实现的?

    带着问题,我们开始阅读源码。

    开始阅读源码

    首先,对用户输入的 reducers 对象进行一次遍历。确保每一项都有对应的 reducer ,并做一份拷贝至 finalReducers 。之后的操作都将会使用检查过的 finalReducers

    对应源码地址。

    export default function combineReducers(reducers: ReducersMapObject) {
      const reducerKeys = Object.keys(reducers)
      const finalReducers: ReducersMapObject = {}
      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 进行传递 undefined 检查。保存捕获到的错误(如果有的话)。

    对应源码地址

    let shapeAssertionError: Error
    try {
      assertReducerShape(finalReducers)
    } catch (e) {
      shapeAssertionError = e
    }
    

    我们来看看 assertReducerShape 的实现。

    对应源码地址

    function assertReducerShape(reducers: ReducersMapObject) {
      // 对 reducers 进行遍历
      Object.keys(reducers).forEach(key => {
        const reducer = reducers[key]
        // 给 reducers 传 undefined,看它是否会返回 initial state
        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.` // 这里说明了,如果要给值设为空,应该设置其为 null 而不是 undefined
          )
        }
    		// 给 reducer 未知的 action,看它是否会直接返回 state
        if (
          typeof reducer(undefined, {
            type: ActionTypes.PROBE_UNKNOWN_ACTION()
          }) === '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.`
          )
        }
      })
    }
    

    如代码中注释所写。assertReducerShapereducers 进行遍历,检查每一个 reducer 是否会在 init 时返回 initial state,并且处理未知的 action 时能够直接返回 state

    然后,combineReducers 返回一个函数 combination 。首先进行一些错误处理。

    对应源码地址

    return function combination(
    	state: StateFromReducersMapObject<typeof reducers> = {},
      action: AnyAction
    	) {
      // 抛出 shapeAssertionError,如果有的话
      if (shapeAssertionError) {
        throw shapeAssertionError
      }
    	// 在非生产环境警告「获取到与预期的 state 结构不一致的问题」
      if (process.env.NODE_ENV !== 'production') {
        const warningMessage = getUnexpectedStateShapeWarningMessage(
          state,
          finalReducers,
          action,
          unexpectedKeyCache
        )
        if (warningMessage) {
          warning(warningMessage)
        }
      }
    

    shapeAssertionError 不必赘述。至于 getUnexpectedStateShapeWarningMessage ,先看看其实现。

    对应的源码地址

    function getUnexpectedStateShapeWarningMessage(
      inputState: object,
      reducers: ReducersMapObject,
      action: Action,
      unexpectedKeyCache: { [key: string]: true }
    ) {
      const reducerKeys = Object.keys(reducers) // reducer 的 namespace
      // 问题来源,根据 action 的类型判断
      const argumentName =
        action && action.type === ActionTypes.INIT
          ? 'preloadedState argument passed to createStore'
          : 'previous state received by the reducer' 
    	// 	如果没有 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)) {
        const match = Object.prototype.toString // 注意这里的 `toString()` 调用方式
          .call(inputState)
          .match(/\s([a-z|A-Z]+)/)
        const matchType = match ? match[1] : ''
        return (
          `The ${argumentName} has unexpected type of "` +
          matchType +
          `". Expected argument to be an object with the following ` +
          `keys: "${reducerKeys.join('", "')}"`
        )
      }
    	// 检查 inputState 中是否有额外的属性
      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.`
        )
      }
    }
    

    判断是否为「纯对象」,并且检查是否有额外的属性。输出对应的错误信息。实现细节参考注释。

    最后,把 state 交给每一个 reducer ,获取到新的 nextState 。并且在过程中检查 nextStateForKey 不要为 undefined 。返回最终的 state 。代码比较简单,阅读难度不大。

    对应的源码地址

        let hasChanged = false // 是否改变的标志位
        const nextState: StateFromReducersMapObject<typeof reducers> = {}
        // 遍历所有的 reducers
        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) // 从 reducer 计算得到 nextState
          // undefined 处理
          if (typeof nextStateForKey === 'undefined') {
            const errorMessage = getUndefinedStateErrorMessage(key, action)
            throw new Error(errorMessage)
          }
          nextState[key] = nextStateForKey
          // 看 state 是否改变,同时使用短路优化
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        hasChanged = // 值没有变,但是非纯对象部分被去除的情况
          hasChanged || finalReducerKeys.length !== Object.keys(state).length
        return hasChanged ? nextState : state
      }
    }
    

    Reference

    • combineReducers | Redux

    • redux - GitHub


    起源地下载网 » 深入Redux源码(二)combineReducers

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元