那么 useSyncExternalStore 的诞生并非偶然,和 v18 的更新模式下外部数据的 tearing 有着十分紧密的关联。
useSyncExternalStore 出现解决了这个问题,我们从 v18 发布的 tag 中,找到这样的描述:
useSyncExternalStore is a new hook that allows external stores to support concurrent reads by forcing updates to the store to be synchronous. It removes the need for useEffect when implementing subscriptions to external data sources, and is recommended for any library that integrates with state external to React.
useSyncExternalStore 能够让 React 组件在 concurrent 模式下安全地有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。当读取到外部状态发生了变化,会触发一个强制更新,来保证结果的一致性。
现在用 useSyncExternalStore 不再需要把订阅到更新的流程交给组件处理。如下:
function App(){
const state = useSyncExternalStore(store.subscribe,store.getSnapshot)
return <div>...</div>
}如上是通过 useSyncExternalStore 实现的订阅更新,这样减少了 APP 内部组件代码,代码健壮性提升,一定程度上也降低了耦合,最重要的它解决了并发模式状态读取问题。但是这里强调的一点是, 正常的 React 开发者在开发过程中不需要使用这个 api ,这个 hooks 主要是对于 React 的一些状态管理库,比如 redux ,通过它的帮助可以合理管理外部的 store,保证数据读取的一致。
接下来看一下 useSyncExternalStore 使用:
useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
)subscribe 为订阅函数,当数据改变的时候,会触发 subscribe,在 useSyncExternalStore 会通过带有记忆性的 getSnapshot 来判别数据是否发生变化,如果发生变化,那么会强制更新数据。
getSnapshot 可以理解成一个带有记忆功能的选择器。当 store 变化的时候,会通过 getSnapshot 生成新的状态值,这个状态值可提供给组件作为数据源使用,getSnapshot 可以检查订阅的值是否改变,改变的话那么会触发更新。
getServerSnapshot 用于 hydration 模式下的 getSnapshot。
接下来我们用 useSyncExternalStore 配合 redux ,来简单实现订阅外部数据源功能。
import { combineReducers , createStore } from 'redux'
/* number Reducer */
function numberReducer(state=1,action){
switch (action.type){
case 'ADD':
return state + 1
case 'DEL':
return state - 1
default:
return state
}
}
/* 注册reducer */
const rootReducer = combineReducers({ number:numberReducer })
/* 创建 store */
const store = createStore(rootReducer,{ number:1 })
function Index(){
/* 订阅外部数据源 */
const state = useSyncExternalStore(store.subscribe,() => store.getState().number)
console.log(state)
return <div>
{state}
<button onClick={() => store.dispatch({ type:'ADD' })} >点击</button>
</div>
}有了 useSyncExternalStore 这个 hooks ,可以通过外部数据到内部数据的映射,当数据变化的时候,可以通知订阅函数 subscribe 去触发更新。
接下来看一下 useSyncExternalStore 内部是如何实现的。
react-reconciler/src/ReactFiberHooks.new.js
function mountSyncExternalStore(subscribe,getSnapshot){
/* 创建一个 hooks */
const hook = mountWorkInProgressHook();
/* 产生快照 */
let nextSnapshot = getSnapshot();
/* 把快照记录下来 */
hook.memoizedState = nextSnapshot;
/* 快照记录在 inst 属性上 */
const inst = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
/* 用一个 effect 来订阅状态 ,subscribeToStore 发起订阅 */
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
/* 用一个 useEffect 来监听组件 render ,只要组件渲染就会调用 updateStoreInstance */
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
undefined,
null,
);
return nextSnapshot;
}mountSyncExternalStore 大致流程是这样的:
subscribeToStore 发起订阅 。updateStoreInstance 。这一步是关键所在,在 concurrent 模式下渲染会中断,那么如果中断恢复 render ,那么这个 effect 就解决了这个问题。当 render 就会触发 updateStoreInstance 。接下来看一下 subscribeToStore 和 updateStoreInstance 的实现。
subscribeToStore
react-reconciler/src/ReactFiberHooks.js
function checkIfSnapshotChanged(inst) {
const latestGetSnapshot = inst.getSnapshot;
/* 取出上一次的快照信息 */
const prevValue = inst.value;
try {
/* 最新的快照信息 */
const nextValue = latestGetSnapshot();
/* 返回是否相等 */
return !is(prevValue, nextValue);
} catch (error) {
return true;
}
}
/* 直接发起调度更新 */
function forceStoreRerender(fiber) {
scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
}
function subscribeToStore(fiber, inst, subscribe) {
const handleStoreChange = () => {
/* 检查 state 是否发生变化 */
if (checkIfSnapshotChanged(inst)) {
/* 触发更新 */
forceStoreRerender(fiber);
}
};
/* 发起订阅 */
return subscribe(handleStoreChange);
}subscribeToStore 的流程如下:
updateStoreInstance
react-reconciler/src/ReactFiberHooks.js
function updateStoreInstance(fiber,inst,nextSnapshot,getSnapshot) {
inst.value = nextSnapshot;
inst.getSnapshot = getSnapshot;
/* 检查是否更新 */
if (checkIfSnapshotChanged(inst)) {
/* 强制更新 */
forceStoreRerender(fiber);
}
}通过如上原理分析,我们知道了 useSyncExternalStore 是如何防止 tearing 的了。为了让大家更清楚其流程 ,接下来我们来模拟一个 useSyncExternalStore 的实现。
function useMockSyncExternalStore(subscribe,getSnapshot){
const [ , forceupdate ] = React.useState(null)
const inst = React.useRef(null)
const nextValue = getSnapshot()
inst.current = {
value:nextValue,
getSnapshot
}
/* 检测是否更新 */
const checkIfSnapshotChanged = () => {
try {
/* 最新的快照信息 */
const nextValue = inst.current.getSnapshot();
/* 返回是否相等 */
return !Object.is(inst.current.value, nextValue)
} catch (error) {
return true;
}
}
/* 处理 store 改变 */
const handleStoreChange=()=>{
if (checkIfSnapshotChanged()) {
/* 触发更新 */
forceupdate({})
}
}
React.useEffect(()=>{
subscribe(handleStoreChange)
},[ subscribe ])
/* 注意这个 useEffect 没有依赖项 ,每次更新都会执行该 effect */
React.useEffect(()=>{
handleStoreChange()
})
return nextValue
}如上就是 useSyncExternalStore 的模拟实现。
本章节介绍了引入外部数据源的 hooks useSyncExternalStore,包括它的介绍、使用和原理。