跳到主要内容

认识

2024年02月24日
柏拉文
越努力,越幸运

一、认识


useSyncExternalStoreReact 18 引入的一个内置 Hook, 专门用于解决外部数据源(比如 Redux``、Zustand 等状态管理库)与 React 组件之间的数据同步问题。它的主要作用是让组件在渲染期间能够安全、一致地读取外部 store 的状态快照, 同时在 store 发生变化时,自动触发组件重新渲染,从而确保 UI 始终与外部状态保持同步。具体来说,它需要你提供两个核心函数:

  • subscribe: 一个函数, 接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变时会调用提供的 callback,这将导致 React 重新调用 getSnapshot 并在需要的时候重新渲染组件。subscribe 函数会返回清除订阅的函数。换句话说: subscribe 的作用就是让每个使用 store 的组件都能注册自己的订阅回调。当 store 更新时, 我们手动调用订阅回调, 从而让相应的组件通过 getSnapshot 获取最新状态并触发重新渲染。所以 subscribe 注册的回调数目实际上就反映了当前有多少组件(或其他订阅者)在关注这个 store 的变化,同时它也支持取消订阅以避免不必要的更新。

  • getSnapshot: 一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。

  • 返回值: 该 store 的当前快照,可以在你的渲染逻辑中使用。

如果我们更新了 store, 循环遍历 subscribe 注册的回调, 会通知 React 重新调用 getSnapshot 并在需要的时候重新渲染组件。这样,即使在并发渲染模式下,也能防止出现 撕裂tearing)现象,保证组件在整个渲染周期内读取到的数据是一致且稳定的。总之,useSyncExternalStore 为外部状态和 React 渲染之间提供了一种标准化、健壮且高效的连接方式,是实现精准订阅和高性能更新的关键工具。

二、语法


const state = { count: 1 };

const listener = new Set();

const subscribe = (callback)=>{
listener.add(callback);
return () => listeners.delete(listener);
}

const getSnapshot = ()=> state;

const setState = ()=>{
const previousState = state;
state = { ...state, count: 2};

listener.forEach(callback => callback(state, previousState));
}

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe: 一个函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的 callback。这会导致组件重新渲染。subscribe 函数会返回清除订阅的函数。

  • getSnapshot: 一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。

  • getServerSnapshot: 一个函数,返回 store 中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的 hydration 时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。

  • snapshot: 该 store 的当前快照,可以在你的渲染逻辑中使用。

三、Polyfill


React 17 中没有 useSyncExternalStore,我们通常会通过自定义 Hook 或使用官方提供的 polyfill 来实现类似的功能。基本思路是用 useState 来保存外部 store 的快照,然后利用 useEffect 订阅 store 的更新。在订阅回调中调用 getSnapshot 获取最新状态,再用 setState 更新组件,从而触发重新渲染。

import { useEffect, useRef, useState } from "react";

function useSyncExternalStore(subscribe, getSnapshot) {
const [snapshot, setSnapshot] = useState(() => getSnapshot());

const getSnapshotRef = useRef(getSnapshot);
getSnapshotRef.current = getSnapshot;

useEffect(() => {
function handleStoreChange() {
const newSnapshot = getSnapshotRef.current();
setSnapshot((prevSnapshot) =>
Object.is(prevSnapshot, newSnapshot) ? prevSnapshot : newSnapshot
);
}
const unsubscribe = subscribe(handleStoreChange);
handleStoreChange();
return unsubscribe;
}, [subscribe]);

return snapshot;
}