我的 redux 解惑之旅

redux 作为 React 全家桶成员经常在项目中使用,在刚接触 redux 时自己总是有些困惑 ? :

  1. reducer 是什么?
  2. 为什么需要 combineReducers?
  3. middlewares 的机制是什么?

通过翻阅 redux 官网,慢慢疑惑被解开
先上一张自己对 redux 整体流程的理解:

redux_process.png

reducer 与可选的 preloadState、applyMiddleware 传入 createStore 创建出拥有 dispatch、getState、subscribe 方法的 store 对象

首先从 reducer 讲起,reducer 其实是更新 state 的具体逻辑

    function reducer(prevState, action) {
        const { type, payload } = action
        
        switch(type) {
            // ...
            default:
                return prevState
        }
    }
复制代码

而 dispatch 通过调用 reducer 来更新 state:

    store.dispatch = (action) => {
        store.state = reducer(store.state, action)
    }
复制代码

所以触发 dispatch 后的流程是 dispatch(action) => reducer(prevState, action) => newState

image.png

当 state 数据结构越来越复杂时,reducer 会包含着不同维度数据的更新逻辑,如:

    let state = {
        user: { id: 1, name: 'Daniel Yang', age: 26, avatar: '' },
        role: { employer: true, developer: true, manager: false },
        list: []
    }
    
    function reducer(prevState = state, action) {
        const { type, payload } = action
        switch (type) {
            case 'UPDATE_USER':
                return { ...prevState, user: payload }
            case 'UPDATE_ROLE':
                return { ...prevState, role: payload }
            case 'REMOVE_ROLE':
                return { ...prevState, role: prevState.role.filter(r => r !== payload.id) }
            case 'ADD_LIST':
                return { ...prevState, list: prevState.list.concat(payload) }
            case 'REMOVE_LIST':
                return { ...prevState, list: list.filter(l => l !== payload.id) }
            default:
                return prevState
        }
    }
复制代码

如果我们能把不同维度的数据拆成独立的 reducer,再由某个方法合并起来传进 createStore 创建 store,整体逻辑会清晰很多
于是 combineReducers 应运而生 ? :

    function combineReducers(reducerObjs) {
        return (prevState, action) => {
            const combineState = {}
            
            for (const key of Object.keys(reducerObjs)) {
                // 获取子 reducer 方法和对应的数据
                const reducer = reducerObjs[key]
                const state = prevState[key]
                combineState[key] = reducer(state, action)
            }
            
            return {
                ...prevState,
                ...combineState
            }
        }
    }
复制代码

现在可以这样拆分我们的 reducer:

    function userReducer(prevState, action) {
        const { type, payload } = action
        switch(type) {
            case 'UPDATE_USER':
                return payload
            default:
                return prevState
        }
    }
    
    function roleReducer(prevState, action) {
        const { type, payload } = action
        switch(type) {
            case 'UPDATE_ROLE':
                return payload
            case 'REMOVE_ROLE':
                return { ...prevState, role: prevState.filter(r => r !== payload.id) }
            default:
                return prevState
        }
    }
    
    function listReducer(prevState, action) {
        const { type, payload } = action
        switch(type) {
            case 'ADD_LIST':
                return { ...prevState, list: prevState.concat(payload) }
            case 'REMOVE_LIST':
                return { ...prevState, list: prevState.filter(l => l !== payload.id) }
            default:
                return prevState
        }
    }
    
    const reducer = combineReducers({
        user: userReducer,
        role: roleReducer,
        list: listReducer
    })
复制代码

随着使用的深入,日志记录、错误捕获、异步数据流等都是常见需求
以日志记录为例,如果我们想记录每次发出的 action 和更新后的 state,在现有基础上该如何实现呢?

尝试一:我们可以在每个调用 dispatch 的地方前后加上打印:

    console.info('action: ', action)
    store.dispatch(action)
    console.info('state: ', store.getState())
复制代码

但显然并不适合,每发起一个 dispatch,我们都得在前后手动写上打印方法,方法太原始

尝试二:要不我们重写下 store.dispatch 方法吧 ? :

    const next = store.dispatch
    store.dispatch = action => {
        console.info('action: ', action)
        next(action)
        console.info('state: ', store.getState())
    }
复制代码

现在我们调用 dispatch 方法时都会在前后打印 action 和 state,看起来很完美 : )
但如果我们有多个扩展想应用在 dispatch 期间呢 ? ,比如增加错误捕获:

    const next = store.dispatch
    store.dispatch = action => {
        try {
            next(action)
        } catch (err) {
            console.error(err)
        }
    }
复制代码

尝试三:于是很自然地想到封装 store.dispatch 方法 ? :

    function addLog(store) {
        let next = store.dispatch
        store.dispatch = action => {
            console.info('action: ', action)
            next(action)
            console.info('state: ', store.getState())
        }
    }
    
    function handleError(store) {
        let next = store.dispatch
        store.dispatch = action => {
            try {
                next(action)
            } catch (err) {
                console.error(err)
            }
        }
    }
    
    addLog(store)
    handleError(store)
复制代码

现在用是可以用了,但再想想能否将中间件优化成链式调用,而不是每次使用都需增加一行声明呢?

尝试四:可以返回扩展后的 dispatch ? ,这样可以让多个中间件串联起来,得到最终的 dispatch:

    // 中间件示例
    function middleware(store) {
        let next = store.dispatch
        return action => {
            // ... do something
            return next(action)
        }
    }
    
    function applyMiddleware(store, middlewares) {
        middlewares.forEach(middleware => {
            store.dispatch = middleware(store)
        })
    }
复制代码

尝试五:如果我们将 dispatch 作为参数传递进去,而不是每次在函数内部通过 store.dispatch 获取岂不是更好 ? :

    // 中间件示例
    function middleware(store) {
        return next => {
            return action => {
                // do something
                return next(action)
            }
        }
    }
    
    // 相应地 applyMiddleware 做了调整
    function applyMiddleware(store, middlewares) {
        // 调整中间件顺序,在包裹后执行时正是中间件传进去的顺序
        middlewares = middlewares.reverse()
        let dispatch = store.dispatch
        
        // 前一个 middleware 返回的 dispatch 会作为下一个 middleware 的 next
        middlewares.forEach(middleware => {
            dispatch = middleware(store)(dispatch)
        })
        
        return { ...store, dispatch }
    }
复制代码

这与 redux 源码中 applyMiddleware 的实现很接近了 ? ,相比之下,源码的实现有几个更完善的地方:

  1. 顶层的 store 参数只暴露必需的方法,而不是整个 store 对象;
  2. 如果在 middleware 中直接调用 store.dispatch 而不是 next(action),那么会再次遍历整个中间件链,这对异步的中间件非常有用;
  3. 为了保证只应用中间件一次,它会作用在 createStore() 上而不是 store 本身。

源码的大致实现:重点介绍 applyMiddleware 部分

    function createStore(reducer, preloadState, applyMiddleware) {
        return applyMiddleware(createStore)(reducer, preloadState)
    }

    function applyMiddleware(...middlewares) {
        return (createStore) => (...args) => {
            // 创建 store
            const store = createStore(...args)
            // 只传递必要的方法给 middleware
            const middlewareAPI = {
                getState: store.getState,
                dispatch: (...args) => dispatch(...args)
            }
            const chain = middlewares.map(middleware => middleware(middlewareAPI))
            // 向将组合后的中间件链传入 store.dispatch,并重新赋值 dispatch
            // 如果中间件里直接调用 store.dispatch,会重新走 compose(...chain)(store.dispatch) 方法
            dispatch = compose(...chain)(store.dispatch)
            
            // 返回新的 store 对象
            return {
                ...store,
                dispatch
            }
        }
    }
    
    function compose(...funcs) {
        // 组合中间件链:(e, f, g) => (...args) => e(f(g(...args)))
        return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }
复制代码

最后看下如何通过中间件实现异步数据流,以 redux-thunk 为例:

    function reduxThunkMiddleware(store) {
        return function (dispatch) {
            return function (action) {
                // 如果发出的 action 是一个方法,将约定好的参数传给 action 执行
                // 可以在 action 方法内部决定何时发出 dispatch
                if (typeof action === 'function') {
                    return action(dispatch, store.getState)
                }
                return dispatch(action)
            }
        }
    }
复制代码

现在对于开头提到的问题,你有自己的答案了吗 ? :

  1. reducer 是什么?
  2. 为什么需要 combineReducers?
  3. middlewares 的机制是什么?
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享