React系列useSyncExternalStore怎么应用
这篇文章主要介绍了React系列useSyncExternalStore怎么应用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇React系列useSyncExternalStore怎么应用文章都会有所收获,下面我们一起来看看吧。
useSyncExternalStore 初体验
首先说明,useSyncExternalStore 这个 hook 并不是给我们在日常项目中用的,它是给第三方类库如 Redux、Mobx 等内部使用的。
我们先来看一下官网是怎么介绍 useSyncExternalStore 的。
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 是一个新的钩子,它通过强制的同步状态更新,使得外部 store 可以支持并发读取。它实现了对外部数据源订阅时不在需要 useEffect,并且推荐用于任何与 React 外部状态集成的库。
useSyncExternalStore 这个新的 hook 的用法如下:
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot )
其中,subscribe 是 external store 提供的 subscribe 方法;getSnapshot、getServerSnapshot 是用于获取指定 store 状态的方法,类似于 react-redux 中使用 useSelector 时需要传入的 selector,一个用于客户端渲染,一个用于服务端渲染;返回值就是我们在组件中可用的 store 状态。
看完上面的翻译和 api 介绍,大家是不是有点一脸懵逼呢?????,说实话,我一开始看到这个 hook,也不知道该怎么使用它,翻阅了不少资料之后才知道它的使用正确姿势。接下来,我就结合自己的学习经历,通过几个简单的小 demo,为大家梳理一下 useSyncExternalStore 的用法以及原理:
Concurrent 模式下使用 react-redux-7 出现状态不一致的情形;
Concurrent 模式下使用 react-redux-8 解决状态不一致;
在自定义 external store 中使用 useSyncExternalStore;
Concurrent 模式下使用 react-redux 7
当我们在项目中使用 react-redux-7 版本时,如果开启 Concurrent 模式,并且在 reconcile 过程中断的时候修改 store,那么就有可能会出现状态不一致的情形
在示例中,我们可以很清楚的看到 store 状态不一致情形。
其中,在组件 TextBox 中,我们使用 startTransition 包裹 store 修改状态的代码, 使得更新采用 Concurrent 模式:
const TextBox = () => { const dispatch = useDispatch(); const handleValueChange = (e) => { startTransition(() => { dispatch({ type: "change", value: e.target.value }); }); }; return <input onChange={handleValueChange} />; };
在组件 ShowText 中,我们通过一个 while loop,将组件的 reconcile 过程人为的调整为 > 5ms, 如下:
const ShowText = () => { const value = useSelector((state) => state); const start = performance.now(); while (performance.now() - start < 20) {} return <div>{value}</div>; };
打开 performance 面板,整个过程如下:
更新开始后,有 10 个 ShowText 节点需要 reconcile, 每个节点 reconcile 时需要耗时 20ms 以上,这就导致每个 ShowText reconcile 结束以后都需要中断让出主线程。在协调中断时,修改 store 状态,后续的 ShowText 节点在恢复 reconcile 时,会使用修改以后的 store 状态,导致最后出现状态不一致的情况。
细心的同学可能会好奇上面为什么会出现两次 reconcile,并且最后所有的 ShowText 组件都显示同样的 store 状态。这是因为react 会为每一次更新分配一条 lane,每次 reconcile 只处理指定 lane 的更新。当我们给 TextBox 做第一次 input 时,触发 react 更新, 分配 lane 为 64,然后开始 reconcile。在 reconcile 过程中,又做了两次 input, 触发两次 react 更新, 分配的 lane 为 128、256。lane 为 64 的 reconcile 结束以后,开始处理 lane 为 384(128 + 256, 128 和 256 的优先级一样,一起处理) 的更新,处理时 store 状态为 123, 所有所有的 ShowText 节点在第二次 reconcile 时显示 123。
针对 Concurrent 模式下状态会出现不一致的情形,react-redux 在最新发布的版本 8 中引入了 useSyncExternalStore,修复了这一问题。
Concurrent 模式下使用 react-redux 8
在 useSyncExternalStore-react-redux-8 中,我们使用了 react-redux 最新发布的 8.0.0 版本
在示例中,我们发现修改 store 状态时,不再出现状态不一致的情形。但是很明显,TextBox 的交互出现了卡顿,不再像 useSyncExternalStore-react-redux-7 中那样的流畅。
这是为什么呢?难道没有触发 Concurrent 模式吗?
打开 performance 面板,整个过程如下:
通过上图,我们可以发现 reconcile 过程变为不可中断的。由于 reconcile 过程不可中断,那么 ShowText 节点显示的状态当然就一致了。
通过这个示例,我们可以看到 useSyncExternalStore 解决状态不一致的方式就是将 reconcile 过程变为不可中断。
那 react-redux-8 中是如何使用 useSyncExternalStore 的呢 ?
考虑到 react-redux 的源码实现还是挺复杂的,我们这里通将过一个简单的自定义 external store 来为大家展示 useSyncExternalStore 的用法,方便大家更好的理解这个新的 hook 该怎么样使用。
Concurrent 模式下使用自定义 external store
首先,我们来定义一个非常简单的 external store。类比 redux 和 react-redux,这个简单的 external store 也会提供类似 createStore、useSelector、useDispatch 的功能。
整个 external store 的核心代码如下:
const { useState, useEffect } from 'react'; const createStore = (initialState) => { let state = initialState; const getState = () => state; const listeners = new Set(); // 通过 useDispatch 返回的 dispatch 修改 state 时,会触发 react 更新 const useDispatch = () => { return (newState) => { state = { ...state, ...newState } listeners.forEach(l => l()); } }; // 订阅 state 变化 const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener) }; } return { getState, useDispatch, subscribe } } const useSelector = (store, selector) => { const [state, setState] = useState(() => selector(store.getState())); useEffect(() => { const callback = () => setState(selector(store.getState())); const unsubscribe = store.subscribe(callback); return unsubscribe; }, [store, selector]); return selector(store.getState()); }
在这个 external store 中,我们可以通过 useSelector 获取需要的公共状态,然后通过 useDispatch 返回的 dispatch 去修改公共状态,并触发 react 更新。
在这里,我们是基于发布订阅模式来实现修改公共状态来触发 react 更新。使用 useSelector 时,注册 callback;使用 dispatch 时,修改公共状态,遍历并执行注册的 callback,通过执行 useState 返回的 setState 触发 react 更新。
针对这种情形,我们可以使用 useSyncExternalStore 来改造 useSelector,过程如下:
import { useSyncExternalStore } from 'react'; const useSelectorByUseSyncExternalStore = (store, selector) => { return useSyncExternalStore( store.subscribe, useCallback(() => selector(store.getState()), [store, selector]) ); }
源码分析
useSyncExternalStore 解决状态不一致的方式就是将 reconcile 过程从 Concurrent 模式变为 Sync 模式即同步不可中断。
useSyncExternalStore
关于这一点,我们可以看看 useSyncExternalStore 相关源码,看看它是怎么实现的。
首先是 useSyncExternalStore 在 mount 阶段时要执行的 mountSyncExternalStore 方法。
// 挂载阶段,执行 mountSyncExternalStore function mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { // 当前正在处理的 fiber node var fiber = currentlyRenderingFiber$1; // 挂载阶段,生成 hook 对象 var hook = mountWorkInProgressHook(); // store 的快照 var nextSnapshot; // 判断当前协调是否是 hydrate var isHydrating = getIsHydrating(); if (isHydrating) { // hydrate, 先不用考虑 ... nextSnapshot = getServerSnapshot(); ... } else { // 获取到的新的 store 的值 nextSnapshot = getSnapshot(); ... var root = getWorkInProgressRoot(); ... if (!includesBlockingLane(root, renderLanes)) { // 一致性检查, concurrent 模式下需要进行一致性检查 pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot); } } // hook 对象存储 store 的快照 hook.memoizedState = nextSnapshot; var inst = { value: nextSnapshot, getSnapshot: getSnapshot }; hook.queue = inst; // 相当于 mount 阶段执行 useEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); ... // 标记 Passive$1 副作用,需要在 commit 阶段进行一致性检查,判断store 是否发生变化 pushEffect(HasEffect | Passive$1, updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot), undefined, null); return nextSnapshot; }
在 mountSyncExternalStore 中,主要做了四件事情:
执行 getSnapshot 方法获取当前 store 状态值,并存储在 hook 中;
consistency check - 一致性检查设置,在 render 阶段结束时要进行 store 的一致性检查;
利用 mountEffect,即 useEffect 在 mount 阶段执行的方法,在节点 mount 完成以后执行 store 对外提供的 subscribe 方法进行订阅;
标记 Passive$1 副作用,在 commit 阶段再进行一次 consistency check;
我们再来看一下 subscribeToStore、pushStoreConsistencyCheck、updateStoreInstance 的实现:
subscribeToStore
function subscribeToStore(fiber, inst, subscribe) { // handleStoreChange 方法在我们通过 store 的 dispatch 方法修改 store 时会触发 var handleStoreChange = function () { if (checkIfSnapshotChanged(inst)) { // 如果 store 发生变化,采用阻塞模式渲染 forceStoreRerender(fiber); } }; // 使用 store 提供的 subscribe 方法去订阅 return subscribe(handleStoreChange); } // 用于判断 store 是否发生变化 function checkIfSnapshotChanged(inst) { var latestGetSnapshot = inst.getSnapshot; // 之前的 store 值 var prevValue = inst.value; try { // 新的 store 值 var nextValue = latestGetSnapshot(); // 浅比较 prevValue, nextValue return !objectIs(prevValue, nextValue); } catch (error) { return true; } } // 使用同步阻塞模式渲染 function forceStoreRerender(fiber) { scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); }
subscribeToStore 中通过 store 提供的 subscribe 方法订阅了 store 状态变化。当我们通过 store 提供的 dispatch 方法修改 store 时,store 会遍历依赖列表,按序执行订阅的 callback。此时 handleStoreChange 方法执行,由于 store 状态发生了变化,执行 forceStoreRerender 方法, 手动触发 Sync 阻塞渲染。
pushStoreConsistencyCheck
// 一致性检查配置,如果是 concurrent 模式,会构建一个 check 对象添加到 fiber node 的 updateQueue 对象的 store 数组中 function pushStoreConsistencyCheck(fiber, getSnapshot, renderedSnapshot) { fiber.flags |= StoreConsistency; var check = { getSnapshot: getSnapshot, value: renderedSnapshot }; var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue; if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber$1.updateQueue = componentUpdateQueue; // 收集 check 对象 componentUpdateQueue.stores = [check]; } else { var stores = componentUpdateQueue.stores; // 收集 check 对象 if (stores === null) { componentUpdateQueue.stores = [check]; } else { stores.push(check); } } } // iber tree 的整个协调过程 function performConcurrentWorkOnRoot(root, didTimeout) { ... // 判断采用 concurrent or sync 模式 var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout); var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes); if (exitStatus !== RootInProgress) { // 协调结束 if (exitStatus === RootErrored) { // 出现异常 ... } if (exitStatus === RootFatalErrored) { // 出现异常 ... } if (exitStatus === RootDidNotComplete) { // suspense 挂起 ... } else { // 协调完成 var renderWasConcurrent = !includesBlockingLane(root, lanes); var finishedWork = root.current.alternate; // 如果是 concurrent 模式,需要进行 store 的一致性检查 if (renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork)) { // store 状态不一致,采用同步阻塞渲染 exitStatus = renderRootSync(root, lanes); ... } ... finishConcurrentRender(root, exitStatus, lanes); } ... }
为了保证 store 的状态一致,react 在 mountSyncExternalStore 方法中,先通过 pushStoreConsistencyCheck 给组件节点配置 check 对象,然后在协调完成以后,再遍历一次 fiber tree,基于节点的 check 对象做状态一致性检查。如果发现 store 状态不一致,那么就通过 renderRootSync 方法重新进行一次 Sync 阻塞渲染。
updateStoreInstance
function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot) { inst.value = nextSnapshot; inst.getSnapshot = getSnapshot; if (checkIfSnapshotChanged(inst)) { // 在 commit 阶段,检查 store 是否发生变化,如果发生变化,触发同步阻塞渲染 forceStoreRerender(fiber); } }
在 commit 阶段,需要处理 render 阶段收集的 effect。此时,如果发现 store 发生变化,那么在浏览器渲染之前,还要重新进行一次 Sync 阻塞渲染,以保证 store 状态一致。
看完 mountSyncExternalStore 的实现之后,我们再来看一下 useSyncExternalStore 在 update 阶段要执行的 updateSyncExternalStore 的实现。
function updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { var fiber = currentlyRenderingFiber$1; // 获取 hooke 对象 var hook = updateWorkInProgressHook(); // 获取新的 store 状态 var nextSnapshot = getSnapshot(); ... var inst = hook.queue; updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); if (inst.getSnapshot !== getSnapshot || snapshotChanged || // Check if the susbcribe function changed. We can save some memory by // checking whether we scheduled a subscription effect above. workInProgressHook !== null && workInProgressHook.memoizedState.tag & HasEffect) { fiber.flags |= Passive; pushEffect(HasEffect | Passive$1, updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot), undefined, null); var root = getWorkInProgressRoot(); ... if (!includesBlockingLane(root, renderLanes)) { // 一致性检查配置 pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot); } } return nextSnapshot; }
updateSyncExternalStore 和 mountSyncExternalStore 做的事情差不多,主要做了:
执行 getSnapshot 方法获取当前 store 状态值,并存储在 hook 中;
利用 updateEffect,即 useEffect 在 update 阶段执行的方法,在节点更新完成以后执行 store 对外提供的 subscribe 方法(如果 store 提供的 subscribe 方法没有发生变化,这一步不会执行);
标记 Passive$1 副作用,在 commit 阶段进行一致性检查;
consistency check - 一致性检查设置,在 render 阶段结束时要进行 store 的一致性检查;
通过上面的源码分析,我们可以了解到 useSyncExternalStore 保证 store 状态一致的手段就是协调采用 Sync 不可中断渲染。
为了达到这个目的,useSyncExternalStore 采用了三道保险:
通过 dispatch 修改 store 状态时,强制使用 Sync 同步不可中断渲染;
Concurrent 模式下,协调结束以后会进行一致性检查,如果发现状态不一致,强制重新进行一次 Sync 同步不可中断渲染;
commit 阶段时,再进行一次一致性检查,如果发现状态不一致,强制重新进行一次 Sync 同步不可中断渲染。
关于“React系列useSyncExternalStore怎么应用”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“React系列useSyncExternalStore怎么应用”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注蜗牛博客行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
评论