您好,登錄后才能下訂單哦!
這篇文章主要介紹了React系列useSyncExternalStore怎么應用的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇React系列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;
當我們在項目中使用 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,修復了這一問題。
在 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 該怎么樣使用。
首先,我們來定義一個非常簡單的 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 在 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 的實現:
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 阻塞渲染。
// 一致性檢查配置,如果是 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 阻塞渲染。
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怎么應用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。