一文读懂Redux、React-Redux源码

1,101 阅读12分钟

前前言

朋友们好啊,我是来自推啊前端团队的 jarvis 同学,本次分享的内容是「解读ReduxReact-Redux源码」。如果大家有不同的观点,欢迎在评论区吐槽和指正哦~😝😝😝

写在前面

作为 React 最早的「状态管理库」,Redux一直是一个备受争议的存在,有人喜欢它单一的数据流向和方便管理回朔的优点,有人讨厌它 入门demo 的复杂。但整体来看,Redux 依旧是一个让人叹服精巧的js库。作为 React 曾经的深度用户,确实也是有很多想要分享的事情。

在初次体验Redux后,我在大脑中思考了以下几个问题:

  1. reducerswitch语句为什么要写default分支,不写会引发什么问题?🤔
    const counter = (state, action) => {
      switch (action.type) {
        case 'ADD':
          return state + 1;
        // 诶,我不写就是玩~
        // default:
        //  return state;
      }
    }
    
  2. Redux还不够吗,为什么又蹦跶出来个 React-Redux?🧐
  3. 组件不写connectstore更新 不会触发组件更新,why?🤨
  4. 为什么用Vuex时代码量那么少,而用Redux要写这么多代码?🤨
  5. 我想在action中用异步方法,为什么还要用中间件呢?🤨
  6. 中间件是个锤子哦?🤨
  7. Redux怎么读?为什么有人教我念durex['djʊəreks] 🤣

小伙伴们对此怎么看呢,或许你会很轻松地侃侃而谈,又或者你也正在为此困惑。其实对于这一类问题,最根本的解决方式就是明白其原理,方可拨开迷雾见光明。

接下来我们会结合源码,对 ReduxReact-Redux 的原理一探究竟🧐🧐🧐。

二者的关系

在聊源码之前,我们先说说二者的关系,

Redux 是一个基于js实现的 数据仓库 ,通过手动 订阅更新 可以定义数据改变时的操作。

如果没有 React-Redux,我们使用 Redux 将会是这种情景:

import { createStore } from 'redux';

// 1.创建reducer
function counter(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    default:
      return state;
  }
}
// 2.创建store
const store = Redux.createStore(counter);

// 3.在应用中使用
function App() {
  const [, forceUpdate] = useState({});
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      forceUpdate({});
    });

    return () => unsubscribe();
  }, []);

  const add = () => {
    store.dispatch({
      type: 'ADD',
    });
  };

  return <div onClick={add}>{store.getState()}</div>;
}

嗯?还要手动订阅更新,是不是很麻烦🤔~


我们想简化代码,毕竟在组件里写订阅更新对我们的业务侵入性太高了,于是我们找来了 React-Redux
React-ReduxReactRedux 的中间桥梁,它简化了在 React 中使用 Redux 的成本,让我们可以在 React 组件 中很容易地获取想要的 store 并且自动订阅更新,于是我们的操作将变得很简单:

import { createStore } from 'redux';

// 1.创建reducer
function counter(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    default:
      return state;
  }
}
// 2.创建store
const store = Redux.createStore(counter);

// 3.在根应用中共享状态
ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

// 4.在业务组件使用
const App = connect()((props) => {
  const add = () => { props.dispatch({ type: 'ADD', }); };
  return <div onClick={add}>{store.getState()}</div>;
})

可以看到,我们不用再手动订阅更新,另外通过connect方法处理后的 组件 有了新的props。其实这就是React-Redux最核心的部分。

所以说,Redux并不是专门为React设计的数据仓库,它可以在任何js库中使用。

1. Redux

1.1. 预先了解

  • 函数式编程
    • 纯函数
    • 合成函数(compose)
    • 柯里化
  • 洋葱模型

1.2. 源码(1):createStore

1.2.1. createStore

  • 作用

    • 创建数据仓库store
  • 核心源码

    // 省略了若干代码
    export default function createStore(reducer, preloadedState, enhancer) {
      // 处理边缘情况
      ...
    
      // 中间件
      if (typeof enhancer !== 'undefined') {
          return enhancer(createStore)(reducer, preloadedState)
      }
    
      // 存储reducer,因为后续可能有覆盖操作
      let currentReducer = reducer;
      // 存储当前状态
      let currentState = preloadedState;
      // 存储订阅函数
      let currentListeners;
      // 存储下一批订阅函数
      let nextListeners = currentListeners;
      // 是否在执行reducer
      let isDispatching = false;
    
      // 深拷贝currentListeners赋值给nextListeners
      function ensureCanMutateNextListeners() {}
    
      // 获取当前状态
      function getState(): S {}
    
      // 添加订阅函数
      function subscribe(listener: () => void) {}
    
      // 调用 reducer 生成新的state
      function dispatch(action: A) {}
    
      // 替换reducer
      function replaceReducer(nextReducer);
    
      // 为 观察者模式/响应式 框架提供的可扩展方法,比如Vue
      function observable() {}
    
      // 初始化state
      dispatch({ type: ActionTypes.INIT } as A);
    
      const store = {
        dispatch: dispatch as Dispatch<A>,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable,
      };
      return store;
    }
    

    一个 Store 实例包括五部分,

    • getState: 获取当前状态
    • dispatch: 调用 reducer 生成新的 state
    • subscribe: 添加订阅函数
    • ensureCanMutateNextListeners: 深拷贝 currentListeners 赋值给 nextListeners
    • observable: 为 观察者模式/响应式 框架提供的扩展方法

    🐳🐳🐳 实例初始化完成后自动调用一次dispatch用作store的初始值,值得注意的是此次 actiontypeActionTypes.INIT)是一个拼接后的随机字符串:

    const ActionTypes = {
      INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
      // 还有另外两个,也是伴随着随机数
    };
    

    type是随机字符串,走switch语句的default分支,返回结果就会作为state的初始值,所以不处理default分支可能会造成某些错误💦💦💦。

1.2.2. getState

  • 作用

    • 返回当前的state
  • 核心源码

    function getState() {
      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;
    }
    

    isDispatching是正在执行reducer的标志,reducer用于改变state,所以此时获取state是不安全的。

1.2.3. dispatch

  • 作用

    • 通过reducer改变state
    • 触发所有的订阅更新
  • 核心源码

    function dispatch(action: A) {
      // 处理两种边缘情况,保证参数action可以使用
      ...
    
      try {
        isDispatching = true;
        currentState = currentReducer(currentState, action);
      } finally {
        isDispatching = false;
      }
    
      const listeners = (currentListeners = nextListeners);
      for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i];
        listener();
      }
    
      return action;
    }
    

    接收action作为参数,通过currentReducer返回一个新的statecurrentReducer就是我们写的reducer

    最后将action原样返回,这是为了后续给中间件使用,在中间件部分会详细讲解。

    我们看到:每次返回一个新的state。思考一下,如果每次返回一个新的引用,并且组件不做优化,是不是会引起不必要的渲染呢,😖😖😖切记,这是一个大坑!我们最后会整理。

1.2.4. subscribe

  • 作用

    • 添加订阅函数
  • 核心源码

    function subscribe(listener: () => void) {
      // 处理边缘情况,保证 listener 是一个函数,并且没有在执行reducer
      ...
    
      let isSubscribed = true;
    
      // 添加到nextListeners中,这里面存放的是 订阅函数
      ensureCanMutateNextListeners();
      nextListeners.push(listener);
    
      return function unsubscribe() {
        if (!isSubscribed) {
          return;
        }
    
        if (isDispatching) {
          throw new Error(
            'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api/store#subscribelistener for more details.'
          );
        }
    
        isSubscribed = false;
    
        ensureCanMutateNextListeners();
    
        // 删除当前的订阅函数
        const index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1);
        currentListeners = null;
      };
    }
    

    添加订阅者,返回一个取消订阅的方法。这里的ensureCanMutateNextListeners是何物呢?

1.2.5. ensureCanMutateNextListeners

  • 作用

    • 确保当前可以执行的订阅函数不被改变
  • 核心源码

    let currentListeners = [];
    let nextListeners = currentListeners;
    function ensureCanMutateNextListeners() {
      if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice();
      }
    }
    function subscribe(fn) {
      ensureCanMutateNextListeners(); // 这里做了一次深拷贝
      nextListeners.push(fn); // 在nextListeners数组中添加不会影响currentListeners数组
    }
    

    刚看到这里我是很疑惑的,后来经过多次 「debug调试」 终于发现这是为了处理某种边缘情况:在更新的过程中如果删除或新增订阅者,本次更新不包括新加入的订阅者,下次更新才会带上。

1.3. 源码(2):中间件

经过以上步骤呢,我们也发现了,只有通过dispatch才可以改变state,而dispatch则是调用reducerreducer只是一个单纯的计算器,接收stateaction,返回新的state

由此可以想象,我们没法直接在dispatch中使用 异步方法 改变state

那么接下来,就该中间件登场了,中间件的作用就是 增强dispatch📍

const store = createStore(reducer, applyMiddleware(中间件1, 中间件2, 中间件3, ...))

1.3.1. applyMiddleware

  • 作用

    • 接收多个中间件作为参数
    • 按照 顺序 调用 中间件
    • 最终返回一个新的dispatch(增强后的dispatch
  • 核心源码

    export default function applyMiddleware(...middlewares) {
      return (createStore) => (reducer, preloadedState) => {
        const store = createStore(reducer, preloadedState);
    
        let dispatch = () => {
          throw new Error(
            'Dispatching while constructing your middleware is not allowed. ' +
              'Other middleware would not be applied to this dispatch.',
          );
        };
    
        const middlewareAPI = {
          getState: store.getState,
          dispatch: (action, ...args) => dispatch(action, ...args),
        };
    
        const chain = middlewares.map((middleware) => middleware(middlewareAPI));
    
        dispatch = compose(...chain)(store.dispatch);
    
        return {
          ...store,
          dispatch,
        };
      };
    }
    

    这里使用了函数式编程概念中的合成函数compose,也就是 洋葱圈模型,后面我们会看一下源码,这里可以先简单理解为得到一个「增强dispatch」,这个「增强dispatch」每次执行时会额外执行中间件的逻辑。

    我们先整理一下applyMiddleware的步骤:

    1. 首先按照顺序中间件注入核心store,核心store包括getStatedispatch
    2. 接着按照顺序执行中间件,依次返回最新的createStore方法;
    3. 然后通过链式组合中间件dispatch合成一个增强后的dispatch
    4. 最后作为一个完整的store返回。

    所以中间件是一个高阶函数:接收store,返回【返回一个dispatch的函数】的 createStore函数

    下面是两个常用中间件的源码,可以看到都很简洁

    • redux-thunk

      function createThunkMiddleware(extraArgument) {
        return ({ dispatch, getState }) => (next) => (action) => {
          if (typeof action === 'function') {
            return action(dispatch, getState, extraArgument);
          }
      
          return next(action);
        };
      }
      
      const thunk = createThunkMiddleware();
      thunk.withExtraArgument = createThunkMiddleware;
      
      export default thunk;
      

      之所以返回了三层函数,其实是redux-thunk的特性所致,redux-thunk允许我们在dispatch中派发一个 函数 用来做异步操作

    • redux-promise

      export default function promiseMiddleware({ dispatch }) {
        return (next) => (action) => {
          if (!isFSA(action)) {
            return isPromise(action) ? action.then(dispatch) : next(action);
          }
      
          return isPromise(action.payload)
            ? action.payload
                .then((result) => dispatch({ ...action, payload: result }))
                .catch((error) => {
                  dispatch({ ...action, payload: error, error: true });
                  return Promise.reject(error);
                })
            : next(action);
        };
      }
      

      这个就比较中规中矩了,返回嵌套两层的函数,用于处理异步的情况。

1.3.2. compose

  • 作用

    • 按照顺序将多个函数组合成一个新函数
  • 核心源码

    export default function compose(...funcs: Function[]) {
      // 下面两个if都是处理边缘情况
      if (funcs.length === 0) {
        return <T>(arg: T) => arg;
      }
    
      if (funcs.length === 1) {
        return funcs[0];
      }
    
      // 通过reduce将函数按照顺序组合起来
      return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
    }
    

    函数式编程中通常用来组合多个函数。比如 compose(a, b, c),最终会得到新函数(...arbs) => a(g(c(...args)))。 可以简单理解为:洋葱最里面的核心就是我们的dispatch,所谓 组合 就是增加了一层层的洋葱圈,每个洋葱圈是一个中间件,最终执行dispatch的时候会依次经过外面的洋葱圈,最后才会到达核心。

未标题-1.jpg

1.4. 源码(3):组合器

组合器也就是 combineReducers

  • 作用

    • 接收多个reducer作为参数
    • 最终返回一个新的reducer(增强后的reducer
    • 新的reducer会依次经过每一个reducer计算,最终返回新的state
  • 核心源码

    export default function combineReducers(reducers) {
      const reducerKeys = Object.keys(reducers);
      const finalReducers = {};
      // 保留符合规范的reducer
      for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i];
    
        if (typeof reducers[key] === 'function') {
          finalReducers[key] = reducers[key];
        }
      }
    
      // 最终reducer的所有key
      const finalReducerKeys = Object.keys(finalReducers);
    
      // 返回一个新的 reducer
      return function combination(state = {}, action) {
        // 用 hasChanged变量 记录前后 state 是否已经修改
        let hasChanged = false;
        // 声明对象来存储下一次的state
        const nextState = {};
    
        // 遍历 finalReducerKeys
        for (let i = 0; i < finalReducerKeys.length; i++) {
          const key = finalReducerKeys[i];
          const reducer = finalReducers[key];
          const previousStateForKey = state[key];
          // 执行 reducer
          const nextStateForKey = reducer(previousStateForKey, action);
    
          // 一些处理边缘情况的代码
          ...
    
          nextState[key] = nextStateForKey;
          // 两次 key 对比 不相等则发生改变
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
        // 最后的 keys 数组对比 不相等则发生改变
        hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
        return hasChanged ? nextState : state;
      };
    }
    

    拿到所有的reducer,当执行dispatch时从上向下依次经过每个 reducer 处理,因此不可以action.type切记不可以重复!

未标题-1副本.jpg 因为该函数的功能是组合 reducer,所以最终返回一个新的reducer

1.5. 流程梳理

  1. Redux初始化时首先判断是否有使用中间件,没有则通过默认方式初始化,有的话通过applyMiddleware函数进行创建
  2. applyMiddleware接收的参数是默认的createStore方法,最终会返回一个新的createStore方法,其中dispatch的功能会被中间件增强
    • 2.1. 在applyMiddleware内部,将中间件依次执行,获取到中间件函数的数组形式
    • 2.2. 然后通过compose将所有的中间件函数合并成一个dispatch
    • 2.3. 最后将加强dispatch和剩余的store一起返回
  3. combineReducers接收一个对象类型的参数,将多个reducer合并成一个函数,执行dispatch时自顶向下依次通过每一个reducer得到最终的state

2. React-Redux

React-Redux 的原理很简单,但源码为了处理很多边缘情况而显得略微复杂,提取核心,可以总结如下

  1. 创建 Context ,通过 Providerstore 共享给子组件;
  2. 类组件 通过 高阶函数connect完成订阅更新;通过 mapStateToProps 实现props注入、通过 mapDispatchToProps 实现dispatch注入;
  3. 函数组件 通过 useSelector 完成订阅更新、实现state注入;通过 useDispatch 返回最新的 dispatch

2.1. connect

  • 作用

    • connect是一个高阶组件,在组件内部完成订阅、实现 mapStateToPropsmapDispatchToProps
  • 核心原理代码

    export const connect = (mapStateToProps, mapDispatchToProps) => (
      WrapperComponent,
    ) => (props) => {
      const store = useStore();
      const { getState, dispatch } = store;
    
      const forceUpdate = useForceUpdate();
    
      // 客户端渲染使用 useLayoutEffect
      useLayoutEffect(() => {
        const unSubscribe = store.subscribe(() => {
          forceUpdate();
        });
        return () => unSubscribe();
      }, []);
    
      const stateToProps = mapStateToProps(getState());
      let actionToProps = { dispatch };
    
      if (typeof mapDispatchToProps === 'object') {
        actionToProps = bindActionCreators(mapDispatchToProps, dispatch);
      } else if (typeof mapDispatchToProps === 'function') {
        actionToProps = mapDispatchToProps(dispatch);
      }
    
      return <WrapperComponent {...props} {...stateToProps} {...actionToProps} />;
    };
    
    const useForceUpdate = () => {
      const [, forceRender] = useReducer((s) => s + 1, 0);
    
      return forceRender;
    };
    

    源码中实现 mapStateToPropsmapDispatchToProps还是比较复杂的,通过match和工厂函数mapStateToPropsFactoriesmapDispatchToPropsFactories完成,但核心代码就是上面代码的样子,其中bindActionCreators如下

    function bindActionCreators(actionMap, dispatch) {
      const actions = {};
      for (const action in actionMap) {
        actions[action] = bindActionCreator(actionMap[action], dispatch);
      }
      return actions;
    }
    
    function bindActionCreator(action, dispatch) {
      return (...args) => dispatch(action(...args));
    }
    

    值得注意的有两个地方,

    • 1. 函数组件实现forceUpdate, 这也是 官网 推荐的方式。

      const [, forceRender] = useReducer((s) => s + 1, 0);
      
    • 2. 为什么用useLayoutEffect

      源码中在这里做了个判断,服务端渲染时使用useEffect客户端渲染时使用useLayoutEffect

      React中,useLayoutEffect对应componentDidMountcomponentDidUpdate,他们都会同步执行; 而useEffect属于异步执行,即在本次更新阶段结束后,在下一个任务调度中执行。

      这也就意味着在客户端渲染时,如果使用useEffect进行订阅订阅会等更新任务完成后执行,那么在当前更新任务中如果有触发更新的操作将会丢失。

      而服务端渲染时为什么用useEffect呢,有两个原因,其一是服务端渲染useLayoutEffect会报出一个warning;其二是服务端渲染dom已经存在,此时js可能还在加载中,所以不存在调度的延迟问题

2.2. useSelector 和 useDispatch

  • 作用

    • useSelector内部完成订阅,返回想要的state
    • useDispatch返回最新的dispatch
  • 核心原理代码

    export const useSelector = (selector) => {
      const store = useStore();
      const forceUpdate = useForceUpdate();
    
      useLayoutEffect(() => {
        const unSubscribe = store.subscribe(() => {
          forceUpdate();
        });
        return () => unSubscribe();
      }, []);
    
      const ret = selector(store.getState());
      return ret;
    };
    export const useDispatch = () => {
      const store = useStore();
      return store.dispatch;
    };
    
    const useStore = () => {
      const store = useContext(Context);
      return store;
    };
    

3. 思考与总结

  1. ReduxVuex的区别

    • Vuex 依赖于 Vue,虽然实现方式很巧妙,但是脱离 Vue 无法使用;Redux 是一个 Javascript 库,可以在任何地方使用;
    • Vuex 通过 插件 进行扩展,可以借助 洋葱模型 扩展 多个插件Redux 通过 中间件 实现扩展;
    • Vuex 基本没有上手难度;Redux 需要了解 函数式编程 的一些概念,比如compose洋葱模型纯函数等;
    • Vuex 的更新粒度对标 Vue ,属于定向更新;而 Redux 基于发布订阅,更新粒度对标 React,但需要使用 connect 处理后才可以实现 自动更新
  2. 一些注意事项

    • reducer是纯函数,因此不要在里面做一些带副作用的操作,比如发布订阅;
    • mapStateToPropsmapDispatchToProps都有第二个参数[ownProps],如果定义该参数,组件将会监听 Redux store 的变化,否则不监听, ownProps 是当前组件⾃身的 props,如果指定了,那么只要组件接收到新的 props,mapStateToProps和mapDispatchToProps 都会被重新计算,此处需要谨慎使用!
  3. reducer函数中default分支 的意义

    Redux初始化 时会调用一次 init 级dispatch 用来初始化store,这时候就用到了 switch 语句的 default 分支,如果不写,store 的默认值将是 undefined

  4. 单一数据源的优与劣

    或许,单一数据源真的很好管理,并且方便做数据回朔。但每次返回一个新的state,这种行为存在很多隐患。想象一下:我们派发一个dispatch得到一个新的stateReact 不会管当前state真正意义上的内容是否发生改变,它只看到前后两次引用不同所以进行更新,这势必会造成很多不必要的更新。这一点 Redux 像极了 React,反观 Vuex 或者 Mobx 这种单引用的数据仓库表现反而更好。

  5. useEffectuseLayoutEffect的区别

    • useEffect: 处于React 渲染阶段时,在函数组件主体内改变 DOM、添加订阅、设置定时器、记录⽇志以及执 ⾏其他包含副作⽤的操作都是不被允许的,因为这可能会产⽣莫名其妙的 bug破坏 UI 的⼀致性。 使⽤ useEffect 完成副作⽤操作,赋值给 useEffect函数会在组件渲染到屏幕之后延迟执⾏。我们可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃⽣通道。

    • useLayoutEffect: 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调⽤ effect。可以使⽤它来读取 DOM 布局并同步触发重渲染。在浏览器执⾏绘制之前, useLayoutEffect 内部的代码将被同步执行。 尽可能使⽤标准的useEffect避免阻塞视觉更新

投稿来自 【我的React笔记】02. 一文读懂Redux、React-Redux源码