網站首頁 編程語言 正文
前言
有過 React 經驗的開發者可能都使用過 React DevTools。
DevTools 提供了豐富的能力:展示組件樹,組件的 props 與組件中 hook 的值。
React DevTools 是如何檢測當前網頁是否使用 React 以及是如何獲取組件相關的眾多數據呢?
React DevTools 的原理
打開 ReactDOM 代碼時,用 devtools
為關鍵字搜索,你會發現許多與 React DevTools 相關的代碼。
function injectInternals(internals) { if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { // No DevTools return false; } var hook = __REACT_DEVTOOLS_GLOBAL_HOOK__; try { rendererID = hook.inject(internals); // We have successfully injected, so now it is safe to set up hooks. injectedHook = hook; } catch (err) { // ... } // DevTools exists }
在瀏覽器控制臺輸入 __REACT_DEVTOOLS_GLOBAL_HOOK__
詳細看一下這個對象。
這個對象十分復雜,以下的幾個方法倒是很值得關注。
onCommitFiberRoot
onCommitFiberUnmount
onPostCommitFiberRoot
渲染階段
從名稱來看,上面這幾個方法與 ReactDOM 的渲染密切相關。
ReactDOM 在特定的階段會調用這些的方法,比如:onCommitFiberRoot
。
function onCommitRoot(root, priorityLevel) { if (injectedHook && typeof injectedHook.onCommitFiberRoot === 'function') { try { // ... injectedHook.onCommitFiberRoot(rendererID, root, priorityLevel, didError); } catch (err) {} } }
正是借助 __REACT_DEVTOOLS_GLOBAL_HOOK__
,React DevTools 便與 ReactDOM 建立起了聯系,從而擁有獲取組件眾多信息的能力。
FiberRoot/FiberNode
在新的 React 架構下,會先把 Virtual DOM 轉成 FiberNode,然后再渲染 FiberNode。
onCommitFiberRoot
等方法中的傳遞的數據正是 FiberNode。
FiberNode 的結構是比較復雜的,可以簡化為如下的結構:
interface ReactFiberRootNode { current: ReactFiberNode; // ... } interface ReactFiberNode { tag: number; stateNode: null | HTMLElement; // dom 節點 memoizedProps?: Record<string, any>; // props memoizedState: ClassComponentState | HookLinkedQueue | null; // hooks child?: ReactFiberNode; sibling?: ReactFiberNode; return: ReactFiberNode; // parent // ... }
從上面的結構可以看出,FiberNode 包含了非常多與組件相關的信息。
stateNode
為組件對應真實的 DOM 節點,memoizedProps
為組件的 props
。
當組件為函數式組件時,tag
為 0,memoizedState
保存了組件中的 hooks 信息。
當組件為類組件時,tag
為 1,memoizedState
則是組件的 state
。
如下圖所示,FiberNode 節點形成一個鏈表結構。
只要能找到組件對應的 FiberNode,我們便可以做到在運行期間以無侵入的方法獲取組件的眾多信息。比如:通過 FiberNode 進行遍歷,實現 findNativeNodesForFiber
方法,用以查找其對應的真實 DOM 節點。
function findNativeNodesForFiber(node?: ReactFiberNode) { // ... // 先遍歷 child const { child } = node; collectStateNode(); // 再遍歷所有的 sibling let current = child?.sibling; while (current) { collectStateNode(); current = current.sibling; } // ... }
React DevTools 中審查元素功能正是基于類似的原理去實現。
memoizedState 與 React Hooks
上文中提到當組件為函數式組件時,memoizedState
保存了 React Hooks 相關的信息。與 FiberNode 類似,React Hooks 也形成一個鏈表。
export interface HookLinkedQueue { memoizedState: any; // 渲染時的值 next: HookLinkedQueue | null; // ... }
React Hook 將其數據都保存在 memoizedState
上。比如對于 useRef
來說,ref.current
值就是 memoizedState
。類似的,可以實現 inspectSomeHooksOfFiber
來獲取組件內使用特定 hook 中保存的值。
function inspectRefHooksOfFiber(node: ReactFiberNode) { let current: HookLinkedQueue | null = node.memoizedState; while (current) { retrieveValue(current); current = current.next; } }
實踐:突破 useDebugValue 的限制
useDebugValue 是 React 內置的一個 hook,用以在 React DevTools 中顯示自定義 hook 的標簽。它的限制是只能在 hook 中使用。借助前文介紹的知識點,我們可以實現一個增加版的 useDebugValue
,你可以像普通的 hook 一樣來使用它,沒有其他限制。
useDebugValueAnywhere 的實現
useDebugValueAnywhere
實現比較簡單,name
表明數據的名稱,用一個特殊的 ref 對象來存儲 debug 相關的數據。
export function useDebugValueAnywhere(name: string, data: any) { const ref = useRef({ [DebugHookKey]: { name, data, }, }); // ... }
特定的 devtools
參考 React DevTools 的邏輯,在 __REACT_DEVTOOLS_GLOBAL_HOOK__
中注入我們的 onCommitFiberRoot
方法,從而確保 ReactDOM 每次渲染時,能獲取最新的 FiberNode。
currentHook.onCommitFiberRoot = function (...args) { handleCommitFiberRoot(...args); // 注入 oldOnCommitFiberRoot.apply(this, args); };
接下來便是對 FiberNode 進行遍歷。在遍歷的過程中,檢查每個 FiberNode 中 memoizedState
鏈表,檢測組件的 hooks 中是否用到了 useDebugValueAnywhere
。
如果存在,就將值 FiberNode 與 hook 中的值保存起來。
{ visitFiberNode(node?: ReactFiberNode) { if (!node) return; this.inspectFiber(node); this.visitFiberNode(node.child); let { sibling } = node; while (sibling) { this.visitFiberNode(sibling); sibling = sibling.sibling; } } }
剩下的工作就是考慮以何種形式去展示收集到的 debug 信息。在 PC 端可以直接輸出數據到控制臺;在移動端 vConsole 使用較多,那么就可以基于 vConsole 開發一個插件,實現一個極簡版的 React DevTools,專門用以展示這些信息。
完整的代碼。
總結
本文剖析了 React DevTools 的原理,介紹隱藏在 ReactDOM 中的一些特性,并帶領大家熟悉了一下 React Fiber 架構?;谏鲜鲈?,可以開發一個增加版的 useDebugValue
。
由于本文介紹的特性并非公開的 API,沒有兼容性。當 React/ReactDOM 版本升級時,還需要再做適配,因此只適合用來開發 DevTools 之類的工具,不推薦業務開發使用。
原文鏈接:https://juejin.cn/post/7138282515596312589
相關推薦
- 2022-05-09 docker-compose容器互相連接的實現_docker
- 2023-03-25 React?數據獲取條件競爭原理解析_React
- 2022-07-10 緩存路由關聯的兩個生命周期activated和deactivated
- 2022-07-02 python列表:開始、結束、步長值實例_python
- 2023-01-15 GoLang內存泄漏原因排查詳解_Golang
- 2022-11-17 Android形狀圖形與狀態列表圖形及九宮格圖片超詳細講解_Android
- 2022-11-20 解析rust中的struct_Rust語言
- 2022-06-19 詳解Rainbond內置ServiceMesh微服務架構_云其它
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支