likes
comments
collection
share

React-Redux 那些年 Redux 官方开的小灶

作者站长头像
站长
· 阅读数 14

关于「你不知道的 Redux」这个专栏

在前端进阶中,redux 或者说是状态管理 是一个比较好的切入点,在实际项目中应用广泛,可选的解决方案非常多,生态非常丰富,学习他可以帮助我们提升编程设计思想,而且代码非常易读,代码量也不大,不像 webpack,react 源码那样庞大,学习收益非常高,同时也是面试中的高频考点。 专栏会持续更新 redux 整个生态的相关知识,包括 react-redux ,redux 中间件,immer 等等,甚至会考虑其他的解决方案,比如最近的 zustand 等。

react-redux 是什么

我们都知道 redux 是一个独立的状态管理工具,并不和任何 react vue 等前端框架绑定,理论上你可以在 vue 或者其他前端框架中使用它。

在 react 中使用 redux 的时候,我们需要使用 react-redux 这个库,那么 react-redux 是什么呢?

为什么需要 react-redux

首先抛出 3 个问题

  • 如何让组件获取 redux 中的数据 和方法
  • 如何让组件获取 redux 中的数据的更新
  • 如何优化组件更新的性能

react-redux 能够很好的解决上面的问题,帮助我们便捷且高性能的使用 redux。

react-redux 的基本使用

react-redux 提供了 Provider 组件,将 redux 的 store 传递给 Provider 组件,然后将 Provider 组件包裹在根组件的外层,这样 Provider 组件的子组件都可以通过 connect 方法获取 redux 中的数据和方法。

import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Class component 版本

import { connect } from "react-redux";

const mapStateToProps = (state) => {
  return {
    count: state.count,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    add: () => dispatch({ type: "add" }),
    reduce: () => dispatch({ type: "reduce" }),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

react-redux 与时俱进,提供了 hooks 版本的 connect 方法 (useSelector 和 useDispatch ),可以让我们在函数组件中使用 redux。

Hooks 版本

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount,
} from "./counterSlice";
import styles from "./Counter.module.css";

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
    </div>
  );
}

react-redux 实现解析

我们可以看到 react-redux 的 API 非常简单,但是它是如何帮实现上面三个能力的呢?

Provider

核心逻辑有两段

  1. 创建一个 context,用来存储 store 和 subscription,以便在子组件中获取 store 和 subscription
  2. 在子组件挂载的时候,订阅 store 的变化,当 store 变化的时候,通过 subscription 通知子组件更新

在最早的时候,我会认为当 store 更新之后,直接更新 context 中的 value 的值,然后触发对应变化的子组件更新,但是这样会导致所有的子组件都会更新,这样的话性能会很差,需要在子组件中使用 shouldComponentUpdate 来判断是否需要更新。

function Provider<A extends Action = AnyAction, S = unknown>({
  store,
  context,
  children,
  serverState,
}: ProviderProps<A, S>) {
  const contextValue = useMemo(() => {
    const subscription = createSubscription(store);
    return {
      store,
      subscription,
      getServerState: serverState ? () => serverState : undefined,
    };
  }, [store, serverState]);

  const previousState = useMemo(() => store.getState(), [store]);

  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue;
    subscription.onStateChange = subscription.notifyNestedSubs;
    subscription.trySubscribe();

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs();
    }
    return () => {
      subscription.tryUnsubscribe();
      subscription.onStateChange = undefined;
    };
  }, [contextValue, previousState]);

  const Context = context || ReactReduxContext;

  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

export default Provider;

接下来,我们看一下子组件如何获取 store 的数据,并减少一些无效 renders

Hooks 对应的实现比较简单,我们看一下 useSelector 的实现

subscription

在 react-redux 内部提供了一套发布订阅模型,redux 的 store 会发布更新到订阅中心 subscription, 子组件会订阅 subscription 的消息,当 store 变化的时候,会通知所有的订阅者,即子组件会执行对应的更新回调。 这一点必须理解清楚,后面的内容都是基于这个模型的。

这里抛出一个问题,发布订阅 和 观察者模式 是一回事儿吗?

useSelector

解释一下原理

  1. 通过 useReduxContext 获取 context 中的 store, 但是不直接使用,而是通过 useSyncExternalStoreWithSelector 获取 store 中的数据
  2. useSyncExternalStoreWithSelector 会订阅 store 的变化,当 store 变化的时候,会通过 selector 获取 store 中的数据,然后通过 equalityFn 判断 store 中的数据是否发生了变化。
  3. 同时将组件状态更新的 callback 函数传递给 subscription.addNestedSub ,当 store 变化的时候,subscription 会 触发这些 callback ,导致组件状态变化和更新
export function createSelectorHook(
  context = ReactReduxContext
): <TState = unknown, Selected = unknown>(
  selector: (state: TState) => Selected,
  equalityFn?: EqualityFn<Selected>
) => Selected {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () => useContext(context);

  return function useSelector<TState, Selected extends unknown>(
    selector: (state: TState) => Selected,
    equalityFn: EqualityFn<NoInfer<Selected>> = refEquality
  ): Selected {
    const { store, subscription, getServerState } = useReduxContext()!;

    const selectedState = useSyncExternalStoreWithSelector(
      subscription.addNestedSub,
      store.getState,
      getServerState || store.getState,
      selector,
      equalityFn
    );

    useDebugValue(selectedState);

    return selectedState;
  };
}

最后我们花点时间简单了解一下 useSyncExternalStoreWithSelector 的实现

先看 useSyncExternalStore 的实现

  1. 顾名思义 useSyncExternalStore,是使用外部 store 作为数据源, 我们通过 getSnapshot 获取外部 store 的最新值,
  2. 内部使用一个 useState 来存储外部 store 的最新值,同时提供更新 state 的方法 forceUpdate
  3. 在 useEffect 中,将 forceUpdate 传递给外部 store 的订阅中心,当外部 store 变化的时候,会通知所有的订阅者,即会触发 forceUpdate,从而导致组件更新

我们可以简单的理解为,useSyncExternalStore 是通过 getSnapshot 获取外部 store 的数据,使用 useState 存储起来,同时把更新 state 的方法通过 subscribe 传递给外部

function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
  var value = getSnapshot();

  const [inst, forceUpdate] = useState({
      inst: {
        value: value,
        getSnapshot: getSnapshot,
      },
    }),


  useEffect(
    function () {
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({
          inst: inst,
        });
      }

      var handleStoreChange = function () {
        if (checkIfSnapshotChanged(inst)) {
          // Force a re-render.
          forceUpdate({
            inst: inst,
          });
        }
      }; // Subscribe to the store and return a clean-up function.

      return subscribe(handleStoreChange);
    },
    [subscribe]
  );
  useDebugValue(value);
  return value;
}

function checkIfSnapshotChanged(inst) {
  var latestGetSnapshot = inst.getSnapshot;
  var prevValue = inst.value;

  try {
    var nextValue = latestGetSnapshot();
    return !objectIs(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

useSyncExternalStoreWithSelector 又是什么?

因为 store 更新后,subscription 会通知所有使用 useSelector 的组件更新,为了避免大量的无效更新,我们需要告诉哪些不需要更新的组件,你们的快照没有变化,不需要更新,这就是要在上面的 getSnapshot 方法做文章

核心代码为 memoizedSelector 函数

  1. 通过咱们在 useSelector 中传递的 selector 函数,获取 store 的快照, 即 nextSelection ,与 prevSelection 进行比较,如果相同,则返回 prevSelection,避免了一次 render
  2. 对比函数为 useSelector 中的第二个参数 isEqual,默认为 全等比较,也可以使用 shallowEqual
function useSyncExternalStoreWithSelector(
  subscribe,
  getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
) {
  // Use this to track the rendered snapshot.
  var instRef = useRef(null);
  var inst;

  inst = instRef.current;

  var _useMemo = useMemo(
      function () {
        var hasMemo = false;
        var memoizedSnapshot;
        var memoizedSelection;

        var memoizedSelector = function (nextSnapshot) {
          var prevSnapshot = memoizedSnapshot;
          var prevSelection = memoizedSelection;

          if (objectIs(prevSnapshot, nextSnapshot)) {
            // The snapshot is the same as last time. Reuse the previous selection.
            return prevSelection;
          } // The snapshot has changed, so we need to compute a new selection.

          // The snapshot has changed, so we need to compute a new selection.
          var nextSelection = selector(nextSnapshot); // If a custom isEqual function is provided, use that to check if the data

          if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
            return prevSelection;
          }

          memoizedSnapshot = nextSnapshot;
          memoizedSelection = nextSelection;
          return nextSelection;
        }; // Assigning this to a constant so that Flow knows it can't change.

        // Assigning this to a constant so that Flow knows it can't change.
        var maybeGetServerSnapshot =
          getServerSnapshot === undefined ? null : getServerSnapshot;

        var getSnapshotWithSelector = function () {
          return memoizedSelector(getSnapshot());
        };

        var getServerSnapshotWithSelector =
          maybeGetServerSnapshot === null
            ? undefined
            : function () {
                return memoizedSelector(maybeGetServerSnapshot());
              };
        return [getSnapshotWithSelector, getServerSnapshotWithSelector];
      },
      [getSnapshot, getServerSnapshot, selector, isEqual]
    ),
    getSelection = _useMemo[0],
    getServerSelection = _useMemo[1];

  var value = useSyncExternalStore(subscribe, getSelection, getServerSelection);

  return value;
}

总结

回答开头的三个问题

  1. react-redux 通过 Provider 提供了一个 store 的上下文,子组件通过 useContext 获取 store 和 subscription
  2. useSyncExternalStore 提供了一个 forceUpdate 方法,通过 subscription 传递给外部 store,当外部 store 变化的时候,会通知所有的订阅者,即会触发 forceUpdate,从而接受数据的更新
  3. useSelector 通过 useSyncExternalStoreWithSelector 获取 store 的快照,然后通过 selector 函数获取 store 的切片,避免因为 store 中的其他模块的变化导致组件的无效更新,必要的时候我们可以通过第二个参数 isEqual 来进一步减少无效 render

最后回应一下标题,react-redux 一路走来都得到了 redux 官方的很多支持,提供了很多仅在 React 内部使用的 API,从最早的 context API 到 react 18 提供的 useSyncExternalStore。