use-sync-external-store源码解读笔记

222 阅读3分钟

use-sync-external-store是什么?

在React18及以后,为解决React18并发更新带来的React Tearing(撕裂)问题,React为第三方状态管理库提供了,useSyncExternalStore这个API,使用这个API监听状态并更新渲染,会粗暴的将并发更新改为同步更新,解决带来的React Tearing问题,但是社区的各个状态管理库并没有直接使用useSyncExternalStore这个库,而是使用了use-sync-external-store这个库,因为useSyncExternalStore这个API只有在React18中有提供,在React18以下的版本并没有这个API,use-sync-external-store这个库在React18以下的版本自己实现了useSyncExternalStore这个API。

在使用use-sync-external-store这个库的时候,会检测React是否提供了useSyncExternalStore这个API,如果提供了则会使用React提供的API,否则使用自己实现的API。

image.png

事实上,use-sync-external-store自己实现的就是React仓库中的useSyncExternalStore这个API的代码,只不过独立发布在了npm上。

社区的各个状态管理库也并没有直接用到useSyncExternalStore而是会使用useSyncExternalStoreWithSelector,相比较下,多了两个额外的参数

export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
  subscribe: (onStoreChange: () => void) => () => void,
  getSnapshot: () => Snapshot,
  getServerSnapshot: undefined | null | (() => Snapshot),
  selector: (snapshot: Snapshot) => Selection,
  isEqual?: (a: Selection, b: Selection) => boolean,
): Selection;
  • selector:选择自己需要的状态值(getSnapshot函数调用返回的子集)。
  • isEqual:自己传入的匹配规则

useSyncExternalStore的源码解读

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  const value = getSnapshot();
  
  // forceUpdate用来触发组件re-render
  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)) {
      forceUpdate({inst});
    }
    const handleStoreChange = () => {
      // 这里做了性能优化,会判断前后状态是否变化,如果没有变化则不会re-render
      if (checkIfSnapshotChanged(inst)) {
        forceUpdate({inst});
      }
    };
    // 订阅,把handleStoreChange传入到订阅函数subscribe中,最终在状态管理库中会调用handleStoreChange来触发re-render
    return subscribe(handleStoreChange);
  }, [subscribe]);

  return value;
}

// 工具函数,判断状态是否变化
function checkIfSnapshotChanged<T>(inst: {
  value: T,
  getSnapshot: () => T,
}): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !Object.is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}
// 外部状态管理器
const store = {
  state: { count: 0 },
  listeners: new Set(),
  setState(newState) {
    this.state = newState;
    // 触发订阅该store的组件re-render
    this.listeners.forEach((listener) => listener());
  },
  subscribe(listener) {
    this.listeners.add(listener);
    // 取消订阅
    return () => this.listeners.delete(listener);
  },
  getState() {
    return this.state;
  },
};

function useStore() {
  const state = useSyncExternalStore(
    // 订阅函数
    (listener) => store.subscribe(listener),
    // 获取当前快照
    () => store.getState()
  );

  return state;
}

export default function App() {
  const { count } = useStore();

  return (
    <div>
      Count: {count}
      <button onClick={() => store.setState({ count: count + 1 })}>
        Increment
      </button>
    </div>
  );
}

先看我们自制的一个外部状态管理库,主要用到的有setState(更新状态),subscribe(订阅),getState(获取状态)。

我们将订阅状态的方法和获取状态的方法传入useSyncExternalStore使用,可以看到useSyncExternalStore内部实现,进入函数内部,首先会获取一次state并赋值给value(这一步很重要),通过useState生成一个强制更新的方法(forceUpdate)。

这里的useLayoutEffect的执行时机在useEffect之前,useLayoutEffect监听(subscribe,value,getSnapshot)这三个值,内部会对useState返回的inst值做重新赋值的逻辑,并判断一次需不需要re-render,这一块最主要的点就是监听value的值以及判断需不需要re-render,因为每一次re-render,value的值都会改变,所以useLayoutEffect在每一次re-render都会执行(目的就是重制inst的值,并且在re-render的dom没渲染到页面上时判断一次需不需要重新re-render,解决了状态撕裂的问题【视图数据出现一致】)。

useEffect会在页面第一次渲染和subscribe改变时执行,大多数情况是在页面第一次渲染时执行,这里判断需不需要re-render主要解决的就是第一次渲染时前置的dom已经渲染在视图上了,如果发现有状态更新,就re-render,后续通过return出去的订阅方式执行re-render。