redux源码分析(二)之combineReducers

26 篇文章 1 订阅

前言:上一篇文章讲完了redux的基础核心部分,我们知道在实际开发中,我们会将reducer进行拆分,这样便于代码的阅读与维护,数据管理更加清晰。而通过上一篇文章,我们知道createStore只能接受一个reducer作为参数,如果我们将reducer进行拆分,那么怎么将多个reducer传入createStore函数呢?接下来,我们就来聊聊combineReducers的作用。

首先我们先来看看combineReducers的使用

//管理用户信息相关的reducer
function userReducer(state={},action) {
    return state
}
//管理菜单信息相关的reducer
function menusReducer(state=[],action) {
    return state
}
//reducer合并
const reducers = combineReducers({userReducer,menusReducer})
//这是ES6对象的简写,相当于
//{userReducer:userReducer,menusReducer:menusReducer}
//创建store
const store = createStore(reducers)

现在我们来看看combineReducers内部做了什么?combineReducers.js的源码如下

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'

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

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

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

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

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */
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)

    let unexpectedKeyCache
    if (process.env.NODE_ENV !== 'production') {
        unexpectedKeyCache = {}
    }

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

    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
    }
}

我们直接来看combinReducers函数,它只接受一个参数reducers,这个reducers是什么呢?按照我们上面的使用例子,这个reducers的值就是

{
    userReducer:function(){...},
    menusReducers:function(){...}
}

接下来我们看看函数内部的实现

 //获取传入对象的键名,返回键名的数组
 const reducerKeys = Object.keys(reducers)
 //声明一个空对象,用来保存最终的reducers
 const finalReducers = {}

此时,reducerKeys的值为 ["userReducer","menusReducer"]数组

//遍历reducerKeys
for (let i = 0; i < reducerKeys.length; i++) {
    //获取每一个键名
    const key = reducerKeys[i]
    //如果是在开发环境,则进行判断,如果reducer是undefined,则提示错误信息
    //例如,我们在调用combineReducers是传入{user:undefined}这样一个对象。
    if (process.env.NODE_ENV !== 'production') {
        if (typeof reducers[key] === 'undefined') {
            warning(`No reducer provided for key "${key}"`)
        }
    }
    //判断传入的对象对应键的值是不是一个函数类型
    if (typeof reducers[key] === 'function') {
        //如果是的话,则存入finalReducers里
        finalReducers[key] = reducers[key]
    }
}

经过上面的遍历后,将传入的reducers拷贝给了finalReducers,这里相当于进行了一次深拷贝。

//获取finalReducers对象的键名数组
const finalReducerKeys = Object.keys(finalReducers)

let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
}

//声明一个变量
let shapeAssertionError
try {
    //调用assertReducerShape函数,传入finalReducers对象
    assertReducerShape(finalReducers)
} catch (e) {
    shapeAssertionError = e
}

上面的代码,首先获取finalReducers对象的键名,保存为一个数组;声明一个变量unexpectedKeyCache,如果是开发环境,将其值赋值为一个空对象。接下来声明了一个变量shapeAssertionError,用来保存错误信息。然后调用了一个函数,finalReducers作为参数传入,并且这个函数的调用使用try...catch包裹起来,当有错误的时候讲错误信息赋值给shapeAssertionError,所以我们知道这个函数的调用可能会抛出错误信息,由此猜测函数的作用是用来对finalReducers对象的数据结构进行检测的,也就是我们开始调用combineReducers传入的对象。

assertReducerShape函数的源码如下:

function assertReducerShape(reducers) {
    //获取reducers的键,得到一个数组,然后遍历此数组
    Object.keys(reducers).forEach(key => {
        //获取每个键对应的reducer
        const reducer = reducers[key]
        //调用reducer,获取初始化state,这里我们第一个参数传入undefined,第二个传入的是一个action,
        //type类型是一个随机字符串,上一篇已经讲过,这样做的目的是为了让reducer返回当前state,就是
        //当前传入的state,在这里也就是undefined
        const initialState = reducer(undefined, { type: ActionTypes.INIT })
        //判断initialState的值是不是undefined,如果是的话则报错
        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.`
            )
        }
        /**
         * 在这里,可能很多人会有疑问,传入的state为undefined,执行的时候又走默认返回值,即直接返回state,
         * 这个initialState不肯定是undefined吗?还以上面的使用为例,我们的userReducer如下
         * function userReducer(state={},action) {return state}
         * 我们在声明的时候给state给了默认值,所以当我们传入undefined时,实际上得到的是{}空对象,所以这里
         * 的作用就是为了让我们声明reducer的时候,state必须给一个默认值。或者在调用createStore的时候传入
         * preloadedState参数,也就是state的初始值
         */

        /**
         * 这里跟上面差不多,传入undefined和一个随机的action.type,判断reducer的返回值是否为undefined,
         * 对于你disptach的任何一个action,如果type是没有在reducer里命名的,应该返回当前state,并且当前的
         * state不能是undefined值,但是可以是null
         */
        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.`
            )
        }
    })
}

接下来我们看最后一段代码,我们知道createStore接受的第一个参数reducer必须是一个函数,所以combineReducer的返回值肯定是一个函数。

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
}

第一个判断上面已经讲过,接下我们看看第二个if判断的作用

function getUnexpectedStateShapeWarningMessage(
    inputState,
    reducers,
    action,
    unexpectedKeyCache
) {
    //获取reducers对象的key的数组
    const reducerKeys = Object.keys(reducers)
    //判断action的类型,如果是ActionTypes.INIT,说明是初始化,则此时state是调用createStore时
    //传入的preloadedState初始化值。否则的话就是通过reducer传入的上一次的state值
    const argumentName =
        action && action.type === ActionTypes.INIT
            ? 'preloadedState argument passed to createStore'
            : 'previous state received by the reducer'
    //如果reducerKeys数组的长度为0,则返回一段描述错误的字符串,即我们传入combineReducers的对象是一个空对象{}
    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.'
        )
    }
    //判断state类型,必须是一个纯对象
    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('", "')}"`
        )
    }
    //遍历state,得到不是期望的键的数组
    const unexpectedKeys = Object.keys(inputState).filter(
        //当这个键不是state自身的,是它原型链上的,并且unexpectedKeyCache中不存(unexpectedKeyCache值为{})
        key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
    )
    //将state中不是预期的key加入到unexpectedKeyCache中,值设置为true    
    unexpectedKeys.forEach(key => {
        unexpectedKeyCache[key] = true
    })
    //如果action.type属于替换类型,也就是替换reducer的时候,直接返回,不做下面的检查
    if (action && action.type === ActionTypes.REPLACE) return
    //判断unexpectedKeys的长度是否大于0,如果大于0则返回一个错误的提示信息字符串,
    //这里的作用是,如果你在初始化state上的某个属性不是本身的属性,而是它原型链上的,
    //如果你改变state时,这个属性将被忽略,什么意思呢?举个例子,假如你的初始化state = {age:23},
    //而state的原型对象上有一个属性name:'张三',这时,在应用中,这两个属性你都能访问到,但是,如果你
    //dispatch一次,这个name属性就会丢失了,因为redux更新state是整个替换。
    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.`
        )
    }
}

下面是combineReducers最主要的代码了

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是否改变,初始值为false。然后声明一个nextState变量,同来存放下一次的state,初始化为一个空对象。

接着循环存放reducer对象键的数据,获取每个键名key,如果没有重新命名,这个key就是reducer函数的函数名;然后通过key在存放reducer的对象里取到对应的reducer;获取每个reducer对应的上一次的state值,保存到变量previousStateForKey中;然后通过当前reducer生成下一次的state

const nextStateForKey = reducer(previousStateForKey, action)

reducer里传入的state是上一次的state,所以说我们可以在reducer拿到上一次的state

接下来判断nextStateForKey的值是不是undefined,如果是则抛出错误,也就是说我们在reducer永远不要返回undefined。

getUndefinedStateErrorMessage函数就是用来生成报错信息的,源码如下:

function getUndefinedStateErrorMessage(key, action) {
    //当action存在,将action.type的值赋值给actionType变量
    const actionType = action && action.type
    //action的描述
    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.`
    )
}
nextState[key] = nextStateForKey

将生成的state根据reducer的key保存到根state中,这边有点绕,等一下会举例说明。

接下来是一个判断,判断是否有reducer的state改变过

hasChanged = hasChanged || nextStateForKey !== previousStateForKey

hasChanged的初始值为false,所以第一次是,肯定走后面的表达式,后面的表达式是判断上一次的state和下一次的state是否相等,当第一次循环后,hasChanged的值为true,所以下面的循环都不会走后面的表达式。

最后则是根据hasChanged来返回state,如果没有变化则返回原来的state。如果有变化则返回nextState。

以下面的例子来说明:

function user (state={},action) {
    switch (action.type) {
        //...
        default:
           return state
    }
}

function menus(state{},action) {
    switch (action.type) {
        //...
        default:
           return state
    }
}

const reducers = combineReducers({user,menus})

在这里我们拿到的reducers函数就是combineReducers返回的combination函数,它也是一个reducer函数,combination形成了一个闭包,闭包里存储了我们自己定义的reducer,以对象的形式存储。如下:

finalReducers = {
    user: function user(state,action){...},
    menus: function menus(state,action) {...}
}

当我们调用createStore函数传入combination函数时,里面会初始化执行一次combination函数,这时combination返回的值赋值给了createStore函数的currentState。根据combination函数的执行,我们知道其返回值应该是一个对象,对象的键是对应reducer函数的名,则为下面的值

nextState = {
    user:{},
    menus:{}
}

合并在一起的reducer所管理的state会放在同一个对象,key为reducer名(当然你也可以自己定义),这样我们就可以将reducer迭代划分无限层了。

在这个例子中最终的currentState就是nextState,你也可以将合并的reducer与其他reducer再次进行合并,currentState就是一棵树的根,下面可以划分很多枝,而枝上又可以有更小的枝。所以我们也将次称为store树。

下一篇:redux源码值applyMiddleware

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值