React-Redux 那些年 Redux 官方开的小灶
关于「你不知道的 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
核心逻辑有两段
- 创建一个 context,用来存储 store 和 subscription,以便在子组件中获取 store 和 subscription
- 在子组件挂载的时候,订阅 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
解释一下原理
- 通过 useReduxContext 获取 context 中的 store, 但是不直接使用,而是通过 useSyncExternalStoreWithSelector 获取 store 中的数据
- useSyncExternalStoreWithSelector 会订阅 store 的变化,当 store 变化的时候,会通过 selector 获取 store 中的数据,然后通过 equalityFn 判断 store 中的数据是否发生了变化。
- 同时将组件状态更新的 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 的实现
- 顾名思义 useSyncExternalStore,是使用外部 store 作为数据源, 我们通过 getSnapshot 获取外部 store 的最新值,
- 内部使用一个 useState 来存储外部 store 的最新值,同时提供更新 state 的方法 forceUpdate
- 在 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 函数
- 通过咱们在 useSelector 中传递的 selector 函数,获取 store 的快照, 即 nextSelection ,与 prevSelection 进行比较,如果相同,则返回 prevSelection,避免了一次 render
- 对比函数为 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;
}
总结
回答开头的三个问题
- react-redux 通过 Provider 提供了一个 store 的上下文,子组件通过 useContext 获取 store 和 subscription
- useSyncExternalStore 提供了一个 forceUpdate 方法,通过 subscription 传递给外部 store,当外部 store 变化的时候,会通知所有的订阅者,即会触发 forceUpdate,从而接受数据的更新
- useSelector 通过 useSyncExternalStoreWithSelector 获取 store 的快照,然后通过 selector 函数获取 store 的切片,避免因为 store 中的其他模块的变化导致组件的无效更新,必要的时候我们可以通过第二个参数 isEqual 来进一步减少无效 render
最后回应一下标题,react-redux 一路走来都得到了 redux 官方的很多支持,提供了很多仅在 React 内部使用的 API,从最早的 context API 到 react 18 提供的 useSyncExternalStore。
转载自:https://juejin.cn/post/7244733519947071543