【源码共读】学习 redux 源码整体架构,深入理解 redux 及其中间件原理

863 阅读3分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

【若川视野 x 源码共读】学习 redux 源码整体架构,深入理解 redux 及其中间件原理 点击了解本期详情一起参与

今天阅读的是:Redux

github.com/reduxjs/red…

image-20230116105813678

当前版本为4.2.0

  • Redux是用来做状态管理的,与我们熟知的其他状态管理工具库,如vuex,pinia,Mobx等类似

尝试使用

  • 打开package.json,我们可以看到build命令是执行rollup编译打包

image-20230116114143319

  • 打开rollup.config.js开启sourceMap,然后运行编译

image-20230116151308825

npm i
npm build

打包后可以在dist中看到打包产物

image-20230116151510835

以计数器为例,创建html文件

<!DOCTYPE html>
<html>
    <head>
        <title>Redux basic example</title>
        <script src="../dist/redux.js"></script>
    </head>
    <body>
        <div>
            <p>
                Clicked: <span id="value">0</span> times
                <button id="increment">+</button>
                <button id="decrement">-</button>
                <button id="incrementIfOdd">Increment if odd</button>
                <button id="incrementAsync">Increment async</button>
            </p>
        </div>
        <script>
            // 定义store
            const actionTypes = {
                INCREMENT: 'INCREMENT',
                DECREMENT: 'DECREMENT'
            }
            const initialState = {
                counter: 0
            }

            function reducer(state = initialState, action) {
                switch (action.type) {
                    case actionTypes.INCREMENT:
                        return { ...state, counter: state.counter + 1 }
                    case actionTypes.DECREMENT:
                        return { ...state, counter: state.counter - 1 }
                    default:
                        return state
                }
            }

            const store = Redux.createStore(reducer)
            const countElement = document.getElementById('value')

            function render() {
                const { counter } = store.getState()
                countElement.innerHTML = counter.toString()
            }

            render()
            store.subscribe(render)
            // 监听按钮事件
            document
                .getElementById('increment')
                .addEventListener('click', function () {
                store.dispatch({ type: actionTypes.INCREMENT })
            })

            document
                .getElementById('decrement')
                .addEventListener('click', function () {
                store.dispatch({ type: actionTypes.DECREMENT })
            })

            document
                .getElementById('incrementIfOdd')
                .addEventListener('click', function () {
                if (store.getState() % 2 !== 0) {
                    store.dispatch({ type: actionTypes.INCREMENT })
                }
            })

            document
                .getElementById('incrementAsync')
                .addEventListener('click', function () {
                setTimeout(function () {
                    store.dispatch({ type: actionTypes.INCREMENT })
                }, 1000)
            })
        </script>
    </body>
</html>


开始调试

  • 对应地方打上断点,按F5进入调试

image-20230116154503887

  • 按下F11进去createStore函数中
export default function createStore<
    S,
    A extends Action,
    Ext = {},
    StateExt = never
>(
reducer: Reducer<S, A>,
 preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
 enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
    // 省略...
    //   合法性校验

    //   当前的reducer
    let currentReducer = reducer
    //   当前的state
    let currentState = preloadedState as S
    //   当前store中的监听函数
    let currentListeners: (() => void)[] | null = []
    //   下一次dispatch的监听函数
    let nextListeners = currentListeners
    //   是否在派发状态
    let isDispatching = false

    //   异常兜底
    function ensureCanMutateNextListeners() {}

    //   获取最新的state
    function getState(): S {}

    //   订阅
    function subscribe(listener: () => void) {}

    //   派发事件
    function dispatch(action: A) {}

    //   替换当前的reducer
    function replaceReducer<NewState, NewActions extends A>(
    nextReducer: Reducer<NewState, NewActions>
    ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {}

    //   提供一个最小的可观察接口
    function observable() {}

    //   初始化
    dispatch({ type: ActionTypes.INIT } as A)

    const store = {
        dispatch: dispatch as Dispatch<A>,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
    } as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
    return store
}

源码解析

我们可以看到,整个store中包含了几个部分

  • state

  • subscribe

  • subscribe

  • getState

  • dispatch

  • replaceReducer(开发库使用)

  • observable(开发库使用)


getState

  • 返回最新的state
  /**
   * Reads the state tree managed by the store.
   *
   * @returns The current state tree of your application.
   */
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState as S
  }

subscribe

function subscribe(listener: () => void) {
    // 省略校验部分...
    let isSubscribed = true
    ensureCanMutateNextListeners()
    // 添加监听函数到下一次dispatch中
    nextListeners.push(listener)
    // 返回取消订阅函数
    return function unsubscribe() {
        // 省略校验部分...
        isSubscribed = false

        ensureCanMutateNextListeners()
        // 从下一轮dispatch队列中剔除
        // 重置当前队列
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
        currentListeners = null
    }
}

ensureCanMutateNextListeners

  • 利用slice做一个浅拷贝数组,切断引用
  /**
   * This makes a shallow copy of currentListeners so we can use
   * nextListeners as a temporary list while dispatching.
   *
   * This prevents any bugs around consumers calling
   * subscribe/unsubscribe in the middle of a dispatch.
   */
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

image-20230116164335265

dispatch

  function dispatch(action: A) {
    // 省略校验部分...

    try {
      isDispatching = true
        // 调用reducer,获取最新的state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    // 通知所有的listeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

综上,createStore提供一个全局的状态state,并提供了dispatch()方法统一的操作state,并通知所有的订阅者(执行所有的listeners)达到更新页面并渲染的目的。


中间件 applyMiddleware

现在有个需求,我需要打印每次派发的action,做一个日志打印的功能

我们需要对每次的action做一个拦截并打印,随后释放出去,执行action

这时候需要引入中间件

  • 页面中添加log方法
function log(store) {
    // 存储起来store
    const next = store.dispatch
    function logAndDispatch(action) {
        console.log('当前派发的action', action)
        // dispatch
        next(action)
        console.log('派发后的结果', store.getState())
    }
    // monkey patch
    store.dispatch = logAndDispatch
}

log(store)

image-20230116171856537

通过上述函数,即可实现功能。但我们需要考虑一个情况,如果存在多个中间件,每次都要这样调用,代码可读性会很差,我们可以试着对他们执行做一层封装

function applyMiddleware(store, ...fns) {
    fns.forEach((fn) => {
        fn(store)
    })
}

这就是简易版的applyMiddleware实现,接下来,我们来看看redux提供的函数

function applyMiddleware() {
    // 数组存储中间件函数
    for (var _len = arguments.length, middlewares = new Array(_len), _key = 0; _key < _len; _key++) {
        middlewares[_key] = arguments[_key];
    }

    return function (createStore) {
        return function (reducer, preloadedState) {
            var store = createStore(reducer, preloadedState);

            var _dispatch = function dispatch() {
                throw new Error('Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.');
            };

            var middlewareAPI = {
                getState: store.getState,
                dispatch: function dispatch(action) {
                    for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
                        args[_key2 - 1] = arguments[_key2];
                    }

                    return _dispatch.apply(void 0, [action].concat(args));
                }
            };
            var chain = middlewares.map(function (middleware) {
                return middleware(middlewareAPI);
            });
            // 将store传入,执行middleware
            _dispatch = compose.apply(void 0, chain)(store.dispatch);
            return _objectSpread2(_objectSpread2({}, store), {}, {
                dispatch: _dispatch
            });
        };
    };
}

综上看,中间件有点像是koa当中的洋葱圈模型,通过不断的compose.apply不断执行middleware

借用川哥文章中的图,redux工作流程图

image.png