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。
事实上,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。