Skip to content

useSyncExternalStore

这个不在 React 内部,而是单独一个 package 维护的一个 API, 主要用于第三方包,提供内置的发布订阅者模式能力,为 React 状态管理库提供了解决方案。

说明

js
const state = useSyncExternalStore(
  store.subscribe,
  () => store.getSnapshot().data
);
  • store.subscribe : 订阅函数,当 state 的值发生变化的时候可以通过这个函数通知订阅者更新
  • () => store.getSnapshot().data : 获取 state 的方法

例子

jsx
const listeners = [];

// 构建一个组件外部 store
const store = {
  inc(num = 1) {
    states = {
      ...states,
      count: states.count + num,
    };
    // 通知订阅者更新
    this.emitChange();
  },
  emitChange() {
    listeners.map((listener) => listener?.());
  },
  // 订阅
  subscribe(listener: () => void) {
    listeners = [...listeners, listener];
    // 返回取消订阅的函数
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  },
  getSnapshot() {
    // 返回属性的快照
    return states;
  },
};

源码阅读

js
export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T
): T {
  // 初始化获取一个当前 store 的快照信息
  const value = getSnapshot();
  // 使用 useState 来提供react中组件更新的能力
  const [{ inst }, forceUpdate] = useState({ inst: { value, getSnapshot } });

  useLayoutEffect(() => {
    // 赋值
    inst.value = value;
    // 缓存 获取快照的函数
    inst.getSnapshot = getSnapshot;
    // 判断是否更新
    if (checkIfSnapshotChanged(inst)) {
      // 如果更新了,那就触发组件的更新
      forceUpdate({ inst });
    }
  }, [subscribe, value, getSnapshot]);

  useEffect(() => {
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({ inst });
    }
    // 提供订阅当前组件的能力,
    const handleStoreChange = () => {
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({ inst });
      }
    };
    return subscribe(handleStoreChange);
  }, [subscribe]);

  useDebugValue(value);
  return value;
}

对于状态管理库,其最重要的是两个方面:

  • 数据的管理 和 判断更新
  • 数据更新后通知订阅者更新

这个 API 主要不是解决第一个问题,即怎么去维护和管理数据,而是主要负责在数据更新后触发当前组件的更新能力,

在 React 中对于函数式组件触发更新的方式主要就是 setState , 所以这个方法的核心也是创建了一个 state,即const [{ inst }, forceUpdate] = useState({ inst: { value, getSnapshot } });

他不关心数据更新后怎么去更新数据,而是提供了一个 forceUpdate 方法去触发组件的更新能力,这样当第二个参数 () => store.getSnapshot().data 返回的新旧 state 的值发生更新的时候,就可以通过 forceUpdate 去触发当前组件的重新渲染了,这时候就引发了下面几个问题:

  1. 新旧 state 值怎么判断是否更新

具体看下面这个方法

js
function checkIfSnapshotChanged(inst) {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

当通过 inst.getSnapshot() 获取到一个新的 nextValue 的时候 ,通过 is() 方法去判断 新旧 state 是否相同,那么就可以看出新旧值是一个浅比较的逻辑。

  1. 订阅者能力

当我们的 state 的数据发生了更新,那么是不是需要通知所有的订阅者去更新,那我们怎么知道有哪些订阅者呐,

所以useSyncExternalStore(store.subscribe) 的第一个参数就是提供一个订阅 当前 state 更新的订阅者的回调函数 即 store.subscribe = (listener) => this.listeners.push(l) , 这样在 store 中就可以通过 this.listeners.map(l => l()) 的回调方式触发当前组件是否需要更新的能力,即

js
useEffect(() => {
  if (checkIfSnapshotChanged(inst)) {
    // Force a re-render.
    forceUpdate({ inst });
  }

  const handleStoreChange = () => {
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({ inst });
    }
  };
  return subscribe(handleStoreChange);
}, [subscribe]);