【React Hooks原理 - useEffect、useLayoutEffect】

介紹

在實際React Hooks項目中,我們需要在項目的不同階段進行一些處理,比如在頁面渲染之前進行dom操作、數據獲取、第三方加載等。在Class Component中存在很多生命周期能讓我們完成這個操作,但是在React Hooks沒有所謂的生命周期,但是它提供了useEffect、useLayoutEffect來讓我們進行不同階段處理,下面就從源碼角度來聊聊這兩個Hooks。【源碼地址】

前提了解

同其他Hooks一樣(useContext除外),在React18版本之后將其拆分為了mount、update兩個函數,并由Dispatcher在不同階段來執行不同函數。

// 掛載時
const HooksDispatcherOnMount: Dispatcher = {readContext,use,useCallback: mountCallback,useContext: readContext,useEffect: mountEffect,useImperativeHandle: mountImperativeHandle,useLayoutEffect: mountLayoutEffect,useInsertionEffect: mountInsertionEffect,useMemo: mountMemo,useReducer: mountReducer,useRef: mountRef,useState: mountState,useDebugValue: mountDebugValue,useDeferredValue: mountDeferredValue,useTransition: mountTransition,useSyncExternalStore: mountSyncExternalStore,useId: mountId,
};// 更新時
const HooksDispatcherOnUpdate: Dispatcher = {readContext,use,useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useImperativeHandle: updateImperativeHandle,useInsertionEffect: updateInsertionEffect,useLayoutEffect: updateLayoutEffect,useMemo: updateMemo,useReducer: updateReducer,useRef: updateRef,useState: updateState,useDebugValue: updateDebugValue,useDeferredValue: updateDeferredValue,useTransition: updateTransition,useSyncExternalStore: updateSyncExternalStore,useId: updateId,
};

下面介紹主要涉及到兩個文件中內容:

  • react/src/ReactHooks.js
    這個文件主要是定義暴露的給用戶實際使用的Hooks,即我們在組件中通過import { useXXX } from 'react'引入的Hooks。
  • react-reconciler/src/ReactFiberHooks.js
    該文件主要是React內部真正執行的Hooks函數,內部將Hooks拆分為了mount、update兩個函數,并通過Dispatcher在不同階段進行分發如上所示

useEffect

由于React18對于Hooks進行了重新組織,將其拆分為了掛載時和更新時,所以我們也從這兩方面入手介紹。

mount掛載時

源代碼文件路徑:react/src/ReactHooks.js

export function useEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const dispatcher = resolveDispatcher();return dispatcher.useEffect(create, deps);
}

正如我們使用的那樣,useEffect接受兩個參數create、deps。然后通過dispatcher在不同階段進行不同的處理即掛載時執行mountEffect,更新時執行updateEffect,通過上面的HooksDispatcherOnMount/HooksDispatcherOnUpdate映射。

React內部實現的Hooks代碼都在react-reconciler/src/ReactFiberHooks.js文件下。(下面代碼皆省略了DEV環境下的代碼)

// react-reconciler/src/ReactFiberHooks.js
function mountEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {mountEffectImpl(PassiveEffect | PassiveStaticEffect,  // 定義的常量用于標記常規的副作用HookPassive, // 表示是被動類型的Hook常量,不需要用戶主動調用create,deps,);
}function mountEffectImpl(fiberFlags: Flags,hookFlags: HookFlags,create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;currentlyRenderingFiber.flags |= fiberFlags;hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,createEffectInstance(),nextDeps,);
}function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,};if (workInProgressHook === null) {// This is the first hook in the listcurrentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {// Append to the end of the listworkInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}

從代碼能看出,當我們調用useEffect之后,如果是首次掛載,React會通過dispatcher觸發mountEffect函數,在其中調用了mountEffectImpl并傳遞了四個參數來對創建當前節點的hook。

  • PassiveEffect | PassiveStaticEffect: 用于標記副作用的常量,用于區分特性和用途。PassiveEffect 是用于標記常規的副作用,例如 useEffect 中定義的副作用。它表示這個副作用是在組件更新階段執行的,但是不會阻塞瀏覽器的渲染。PassiveStaticEffect 是用于標記靜態的副作用。表示這個副作用是靜態的,不會在組件的多次渲染中發生變化,通常與靜態數據相關。
  • HookPassive:標記Hook的類型常量,在 React 內部,不同類型的 Hook 會根據不同的標記和調度器進行處理。HookPassive 表示這個 Hook 是一種被動的類型,適用于大多數常規的 Hook 使用情況。
  • create:組件內使用useEffect包裹的函數
  • deps:useEffect包裹函數所依賴的參數

在mountEffect中將創建useEffect所需要的數據傳遞mountEffectImpl之后,就進行Hook的創建。在mountEffectImpl函數中主要做了這些操作:

  • 調用mountWorkInProgressHook函數,創建一個管理hooks的循環鏈表
  • 獲取依賴nextDeps,以及設置該副作用的Flag
  • 通過pushEffect創建一個副作用鏈表,并保存在hook.memoizedState中
function pushEffect(tag: HookFlags,create: () => (() => void) | void,inst: EffectInstance, // 組件實例deps: Array<mixed> | null,
): Effect {const effect: Effect = {tag,create,inst,deps,// Circularnext: (null: any),};let componentUpdateQueue: null | FunctionComponentUpdateQueue =(currentlyRenderingFiber.updateQueue: any);if (componentUpdateQueue === null) {componentUpdateQueue = createFunctionComponentUpdateQueue();currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);componentUpdateQueue.lastEffect = effect.next = effect;} else {const lastEffect = componentUpdateQueue.lastEffect;if (lastEffect === null) {componentUpdateQueue.lastEffect = effect.next = effect;} else {const firstEffect = lastEffect.next;lastEffect.next = effect;effect.next = firstEffect;componentUpdateQueue.lastEffect = effect;}}return effect;
}

pushEffect主要是創建一個副作用循環鏈表,并將其掛載在當前渲染fiber節點的狀態更新隊列中。所以fiber.updateQueue.lastEffect 指向的就是pushEffect創建的副作用鏈表。

因為effect list是環狀鏈表,updateQueue.lastEffect指向的最后元素,是因為這樣有利于遍歷時從起點開始,以及更好的插入effect

至此在掛載時,成功創建了hook鏈表和effect鏈表并掛載在當前渲染fiber節點的updateQueue中,后續通過在 Commit 階段,React 會遍歷 Effect list,執行相應的副作用操作。

update更新時

和掛載時類似,updateEffect會調用updateEffectImpl來進行更新處理。

function updateEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}function updateEffectImpl(fiberFlags: Flags,hookFlags: HookFlags,create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;const effect: Effect = hook.memoizedState;const inst = effect.inst;// currentHook is null on initial mount when rerendering after a render phase// state update or for strict mode.if (currentHook !== null) {if (nextDeps !== null) {const prevEffect: Effect = currentHook.memoizedState;const prevDeps = prevEffect.deps;if (areHookInputsEqual(nextDeps, prevDeps)) {hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);return;}}}currentlyRenderingFiber.flags |= fiberFlags;hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,inst,nextDeps,);
}

由上面可以知道pushEffect主要就是創建一個effect然后將其添加到fiber的更新隊列中。而在更新時,通過areHookInputsEqual對比了前后渲染的依賴是否改變,然后通過pushEffect創建新的effect然后添加到更新隊列,區別是當依賴改變時會將當前創建的新的hook的flag設置為HookHasEffect,表示當前副作用需要重新執行。

cleanup清除函數

在useEffect中返回一個函數,該函數會在每次組件更新前以及組件卸載前會執行,該函數稱為清除函數。

useEffect(() => {console.log('useEffect');return () => {consoe.log('清除函數')}
}, [deps])

該函數會在commitHookEffectListMount函數中掛載到effect副作用上,并且在commitHookEffectListUnmount中執行,這兩個函數都是在commit階段進行的,文件路徑為:packages/react-reconciler/src/ReactFiberCommitWork.js

commitHookEffectListMount 負責在副作用更新后重新執行副作用(即deps更新后會觸發該函數執行):

function commitHookEffectListMount(tag: HookFlags,finishedWork: Fiber,
) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);let lastEffect: Effect | null = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {if ((effect.tag & tag) === tag) {// 執行副作用創建函數const create = effect.create;effect.destroy = create();}effect = effect.next;} while (effect !== firstEffect);}
}

從代碼可以看出,在commitHookEffectListMount函數中,如果useEffect副作用中存在清除函數(即return的函數),則會掛載在副作用中,即 effect.destroy = create();

commitHookEffectListUnmount 負責在組件更新或卸載時清理副作用:

function commitHookEffectListUnmount(tag: HookFlags,finishedWork: Fiber,
) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);let lastEffect: Effect | null = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {if ((effect.tag & tag) === tag) {// 執行清理函數const destroy = effect.destroy;if (destroy !== undefined) {destroy();}}effect = effect.next;} while (effect !== firstEffect);}
}

所以當組件卸載或者更新之前,會先執行清除函數然后在重新掛載新的清除函數。

useEffect的執行時機

上面說了在React Hooks中為了讓我們能擁有類似Class Component生命周期一樣對項目運行階段進行監聽并處理的功能,所以有了useEffect鉤子,下面列舉一下useEffect和Class Component生命周期的對應關系,幫助理解useEffect的執行時機。

  • 不寫依賴數組: useEffect 會在每次渲染后執行,類似于 componentDidMount 和 componentDidUpdate 的結合。
  • 空依賴數組: useEffect 只在組件掛載和卸載時執行一次,類似于 componentDidMount 和 componentWillUnmount 的組合。
  • 帶依賴數組: useEffect 只會在組件掛載時和依賴項發生變化時執行,類似于 componentDidUpdate 針對特定依賴項的變化。

可能有的同學看到不寫依賴數組,會在每次渲染以及更新時都會執行,那這樣和不適應useEffect包裹,直接在組件內聲明有什么區別呢?下面也簡單列舉一下:

比較維度直接在函數內的代碼useEffect 中的代碼
執行時機在 React 調用組件函數期間同步執行,這意味著它會在 React 準備和生成新的虛擬 DOM 樹時執行在 React 完成更新后(渲染并提交真實 DOM 變更后)異步執行,適合處理副作用,如數據獲取、訂閱、DOM 操作等
副作用管理不適合處理副作用,邏輯應該是純函數的,不應引起副作用(例如,不直接操作 DOM)專為處理副作用設計,適合處理那些在組件渲染后需要進行的操作,如數據獲取、DOM 更新或事件訂閱
清理機制沒有自動的清理機制。提供了一個清理函數,允許在組件卸載或下一次副作用執行之前進行清理工作。
性能優化每次渲染都會執行。如果不需要每次都執行,會造成不必要的性能開銷。通過依賴數組控制執行頻率,避免不必要的重新執行。

由表可以看出,主要區別在于副作用處理,和性能優化的區別。需要根據場景來決定如何使用,除非需要實時更新執行,否則一般不推薦在組件內直接寫函數。

useLayoutEffect

上面聊了useEffect,下面來談談它的同胞兄弟useLayoutEffect,畢竟我們經常看到說使用useLayoutEffect可以有效解決在useEffect中操作狀態/dom導致的屏幕閃縮問題。

同useEffect一樣,useLayoutEffect也分為了mount、update。所以我們之間步如主題,從mountLayoutEffect開始。

從代碼來看,在mount時useLayoutEffect和useEffect兩者在語法上是一樣的,都接受一個create函數(可包含cleanup函數),一個deps依賴數組,并都通過mountWorkInProgressHook來創建hook,然后通過pushEffect添加effect到hook中。更新時候也和useEffect大致一致,所以這里放在一起,重復代碼則不再冗余貼上了。

// mount
function mountLayoutEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}// update
function updateLayoutEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

區別就是在調用mountEffectImplupdateEffectImpl時傳入的Flags不一樣

// useEffect
const PassiveEffect = /* */ 0b000000001000;// useLayoutEffect
const UpdateEffect = /* */ 0b000000000100;
  • PassiveEffect 表示這是一個被動的副作用,它會在瀏覽器完成布局和繪制后執行。
  • UpdateEffect 表示這是一個同步的副作用,它會在所有 DOM 變更之后,瀏覽器繪制之前執行。

由此能看出useEffect是瀏覽器完成布局和繪制后異步執行,不影響渲染。而useLayoutEffect在所有 DOM 變更之后,瀏覽器繪制之前同步執行。

執行時機: DOM變更完成 -> useLayoutEffect(同步) -> 頁面繪制 -> useEffect(異步)。 這也說明了為什么在useEffect中操作狀態或者DOM時候,屏幕會閃縮(因為頁面已經渲染,然后異步更新狀態之后,會導致頁面再次渲染時候存在時間差)。而useLayoutEffect能解決閃爍問題。(useLayoutEffect在頁面還未繪制之前同步執行,修改狀態之后再繪制到頁面,對用戶來說無感知,但是處理長任務時,會導致白屏問題)。

總結一下。兩種區別主要是執行時機不同:

  • useEffect 使用 PassiveEffect 標志,確保副作用在瀏覽器完成繪制后異步執行。
  • useLayoutEffect 使用 UpdateEffect 標志,確保副作用在 DOM 更新后,瀏覽器繪制前同步執行。

總結

useEffect和useLayoutEffect在語法和代碼組織上,邏輯大致相同。在mount階段通過mountWorkInProgressHook 創建hook,pushEffect創建effect list并綁定在渲染fiber上。在update階段通過updateEffectImpl調用updateWorkInProgressHook更新hook 列表,并通過areHookInputsEqual判斷依賴是否變化,然后設置不同的Flag交給pushEffect創建新的effect,在執行時會根據設置的Flag來判斷是否需要重新執行。

當狀態更新時總的流程如下:

  • count 狀態更新,組件重新渲染。
  • React 計算新的虛擬 DOM 并將其變更應用到實際 DOM。
  • useLayoutEffect 清除函數(如果存在)在 DOM 變更后立即同步執行。
  • useLayoutEffect 的新副作用在 DOM 變更后立即同步執行。
  • 瀏覽器繪制頁面。
  • useEffect 清除函數(如果存在)在繪制完成后異步執行。
  • useEffect 的新副作用在繪制完成后異步執行。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/42190.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/42190.shtml
英文地址,請注明出處:http://en.pswp.cn/web/42190.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python - 類和對象

一.類 類名用大寫字母開頭 屬性是類中的變量&#xff0c;方法是類中的函數 類、class關鍵字&#xff1a; >>> class Turtle: ... color green ... weight 10 ... legs 4 ... shell True ... mount 大嘴 ... def climb(self): ... …

從零開始讀RocketMq源碼(二)Message的發送詳解

目錄 前言 準備 消息發送方式 深入源碼 消息發送模式 選擇發送方式 同步發送消息 校驗消息體 獲取Topic訂閱信息 高級特性-消息重投 選擇消息隊列-負載均衡 裝載消息體發送消息 壓縮消息內容 構造發送message的請求的Header 更新broker故障信息 異步發送消息 …

Open3D KDtree的建立與使用

目錄 一、概述 1.1kd樹原理 1.2kd樹搜索原理 1.3kd樹構建示例 二、常見的領域搜索方式 2.1K近鄰搜索&#xff08;K-Nearest Neighbors, KNN Search&#xff09; 2.2半徑搜索&#xff08;Radius Search&#xff09; 2.3混合搜索&#xff08;Hybrid Search&#xff09; …

ai native 模型微調

AI native 模型微調&#xff08;fine-tuning&#xff09;是指在預訓練模型的基礎上&#xff0c;通過對其參數進行進一步訓練&#xff0c;使其在特定任務上表現更佳。以下是關于模型微調的一些基本步驟和概念&#xff1a; ### 1. 準備數據集 - **數據收集**&#xff1a;收集適用…

后端之路——登錄校驗前言(Cookie\ Session\ JWT令牌)

前言&#xff1a;Servlet 【登錄校驗】這個功能技術的基礎是【會話技術】&#xff0c;那么在講【會話技術】的時候必然要談到【Cookie】和【Session】這兩個東西&#xff0c;那么在這之前必須要先講一下一個很重要但是很多人都會忽略的一個知識點&#xff1a;【Servlet】 什么是…

Oracle PL/SQL 循環批量執行存儲過程

1. 查詢存儲過程 根據數據字典USER_OBJECTS查詢出所有存儲過程。 2. 動態拼接字符串&#xff08;參數等&#xff09; 根據數據字典USER_ARGUMENTS動態拼接參數。 3. 動態執行 利用EXECUTE IMMEDIATE動態執行無名塊。 4. 輸出執行信息 利用DBMS_OUTPUT.PUT_LINE輸出執行成功與…

Android Gradle 開發與應用 (十): Gradle 腳本最佳實踐

目錄 1. 使用Gradle Kotlin DSL 1.1 什么是Gradle Kotlin DSL 1.2 遷移到Kotlin DSL 1.3 優勢分析 2. 優化依賴管理 2.1 使用依賴版本管理文件 2.2 使用依賴分組 3. 合理使用Gradle插件 3.1 官方插件和自定義插件 3.2 插件管理的最佳實踐 4. 任務配置優化 4.1 使用…

Oracle 19c 統一審計表清理

zabbix 收到SYSAUX表空間告警超過90%告警&#xff0c;最后面給出的清理方法只適合ORACLE 統一審計表的清理&#xff0c;傳統審計表的清理SYS.AUD$不適合&#xff0c;請注意。 SQL> Col tablespace_name for a30 Col used_pct for a10 Set line 120 pages 120 select total.…

STM32實戰篇:閃燈 × 流水燈 × 蜂鳴器

IO引腳初始化 即開展某項活動之前所做的準備工作&#xff0c;對于一個IO引腳來說&#xff0c;在使用它之前必須要做一些參數配置&#xff08;例如&#xff1a;選擇工作模式、速率&#xff09;的工作&#xff08;即IO引腳的初始化&#xff09;。 IO引腳初始化流程 1、使能IO引…

LED燈的呼吸功能

"呼吸功能"通常是指 LED 燈的一種工作模式&#xff0c;它模擬人類的呼吸節奏&#xff0c;即 LED 燈的亮度會周期性地逐漸增強然后逐漸減弱&#xff0c;給人一種 LED 在"呼吸"的感覺。這種效果通常用于指示設備的狀態或者簡單地作為裝飾效果。&#xff08;就…

Spring Boot Security自定義AuthenticationProvider

以下是一個簡單的示例&#xff0c;展示如何使用AuthenticationProvider自定義身份驗證。首先&#xff0c;創建一個繼承自標準AuthenticationProvider的類&#xff0c;并實現authenticate方法。 import com.kamier.security.web.service.MyUser; import org.springframework.se…

【Adobe】Photoshop圖層的使用

Adobe Photoshop(簡稱PS)中的圖層是圖像處理中一個核心概念,它允許用戶以堆疊的方式組織圖像的不同部分,從而實現對圖像的復雜編輯和處理而不影響原始圖像。以下是關于Adobe Photoshop圖層的詳細介紹: 一、圖層的定義 圖層就像是透明的紙張,你可以在上面繪制、添加圖像…

YOLOv10改進 | EIoU、SIoU、WIoU、DIoU、FocusIoU等二十余種損失函數

一、本文介紹 這篇文章介紹了YOLOv10的重大改進&#xff0c;特別是在損失函數方面的創新。它不僅包括了多種IoU損失函數的改進和變體&#xff0c;如SIoU、WIoU、GIoU、DIoU、EIOU、CIoU&#xff0c;還融合了“Focus”思想&#xff0c;創造了一系列新的損失函數。這些組合形式的…

Android Init Language自學筆記

Android Init Language由五個元素組成&#xff1a;Acttions、Commands、Services、Options和Imports。 Actions和Services隱式聲明了一個新的section。所以的Commands和Options都屬于最近聲明的section。 Services具有唯一的名稱&#xff0c;如果重名會報錯。 Actions Acti…

解決Spring Boot中的高可用性設計

解決Spring Boot中的高可用性設計 大家好&#xff0c;我是微賺淘客系統3.0的小編&#xff0c;也是冬天不穿秋褲&#xff0c;天冷也要風度的程序猿&#xff01; 1. 高可用性設計概述 1.1 什么是高可用性&#xff1f; 高可用性指系統在面對各種故障和異常情況時&#xff0c;仍…

獨立開發者系列(22)——API調試工具apifox的使用

接口的邏輯已經實現&#xff0c;需要對外發布接口&#xff0c;而發布接口的時候&#xff0c;我們需要能自己簡單調試接口。當然&#xff0c;其實自己也可以寫簡單的代碼調試自己的接口&#xff0c;因為其實就是簡單的request請求或者curl庫讀取&#xff0c;調整請求方式get或者…

如果MySQL出現 “Too many connections“ 錯誤,該如何解決?

當你想要連接MySQL時出現"Too many connections" 報錯的情況下&#xff0c;該如何解決才能如愿以償呢&#xff1f;都是哥們兒&#xff0c;就教你兩招吧&#xff01; 1.不想重啟數據庫的情況下 你可以嘗試采取以下方法來解決&#xff1a; 增加連接數限制&#xff1a…

RxJava學習記錄

文章目錄 1. 總覽1.1 基本原理1.2 導入包和依賴 2. 操作符2.1 創建操作符2.2 轉換操作符2.3 組合操作符2.4 功能操作符 1. 總覽 1.1 基本原理 參考文獻 構建流&#xff1a;每一步操作都會生成一個新的Observable節點(沒錯&#xff0c;包括ObserveOn和SubscribeOn線程變換操作…

asp.netWebForm(.netFramework) CSRF漏洞

asp.netWebForm(.netFramework) CSRF漏洞 CSRF&#xff08;Cross-Site Request Forgery&#xff09;跨站請求偽造是一種常見的 Web 應用程序安全漏 洞&#xff0c;攻擊者通過誘使已認證用戶在受信任的網站上執行惡意操作&#xff0c;從而利用用戶的身份 執行未經授權的操作。攻…

echarts實現3D餅圖

先看下最終效果 實現思路 使用echarts-gl的曲面圖&#xff08;surface&#xff09;類型 通過parametric繪制曲面參數實現3D效果 代碼實現 <template><div id"surfacePie"></div> </template> <script setup>import {onMounted} fro…