【React Hooks原理 - useCallback、useMemo】

介紹

在實際項目中,useCallback、useMemo這兩個Hooks想必會很常見,可能我們會處于性能考慮避免組件重復刷新而使用類似useCallback、useMemo來進行緩存。接下來我們會從源碼和使用的角度來聊聊這兩個hooks。【源碼地址】

為什么要有這兩個Hooks

在開始介紹之前我們先來了解下為什么有這兩個hooks,其解決了什么問題?借用官網案例:

function ProductPage({ productId, referrer, theme }) {// 每當 theme 改變時,都會生成一個不同的函數function handleSubmit(orderDetails) {post('/product/' + productId + '/buy', {referrer,orderDetails,});}return (<div className={theme}>{/* 這將導致 ShippingForm props 永遠都不會是相同的,并且每次它都會重新渲染 */}<ShippingForm onSubmit={handleSubmit} /></div>);
}

每當切換主題theme,ProductPage就會重新渲染,而即使ShippingForm使用memo包裹并且沒有做任何更改也會重新渲染,這就是常說的父組件渲染導致子組件跟著渲染。
再看另一種情況:

function createOptions() {return {serverUrl: 'https://localhost:1234',roomId: roomId};}useEffect(() => {const options = createOptions();const connection = createConnection();connection.connect();}, [createOptions])

在useEffect中添加了createOptions作為依賴,但是createOptions函數每次執行都返回的不同函數導致useEffect會重新執行

所以為了解決類似上面兩種問題,利用緩存封裝了useCallback、useMemo等hooks。

useCallback

function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

讓我們帶著上面這兩個問題來了解useCallback。用白話來說useCallback就是接收一個callback和依賴deps,只要依賴的deps沒有改變,通過useCallback返回的函數就是同一個,以此來避免重復刷新。如果deps改變則useCallback會返回新的callback并將其緩存,以便下次對比。

從源碼來看幾乎所有的Hooks都被拆分為了mount、upadte兩種(useContext除外),React內部會根據當前渲染階段來判斷調用那個來處理callback

// 首次掛載時
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,
};

以下會以useCallback為例,從源碼上一步一步了解。

調用流程

從上面流程圖能看出,當我們在組件內使用useCallback的時候,React會通過dispatcher根據渲染狀態來進行不同的處理。

export function useCallback<T>(callback: T,deps: Array<mixed> | void | null,
): T {return useCallbackImpl(callback, deps);
}function useCallbackImpl<T>(callback: T,deps: Array<mixed> | void | null,
): T {const dispatcher = resolveDispatcher();return dispatcher.useCallback(callback, deps);
}

這里的dispatcher 是一個對象,它會在不同的渲染階段指向不同的實現。在初次渲染時,它會指向 HooksDispatcherOnMount,在更新時,它會指向 HooksDispatcherOnUpdate。

mountCallback

當首次渲染時,會執行mountCallbac返回新的callback并將其和所依賴的deps緩存到memoizedState中

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;hook.memoizedState = [callback, nextDeps];return callback;
}

在首次渲染時候主要做了下列事情:

  • mountWorkInProgressHook: 會創建一個hook并綁定到當前渲染的fiber中
  • 獲取依賴deps,并將callback和deps緩存到當前fiber的hook中

在Function Component中,每個fiber節點都有一個自己的副作用hook list,在協調器(Reconciler)的fiber構造的beginWork階段會將當然fiber節點的hook保存在hook list中,詳情可查看這篇文章:【React架構 - Fiber構造循環】

updateCallback

更新渲染時,會執行updateCallback函數,會根據依賴是否變化來判斷是否使用緩存

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;const prevState = hook.memoizedState;if (nextDeps !== null) {const prevDeps: Array<mixed> | null = prevState[1];if (areHookInputsEqual(nextDeps, prevDeps)) {return prevState[0];}}hook.memoizedState = [callback, nextDeps];return callback;
}

updateCallback主要做了下列事情:

  • 通過updateWorkInProgressHook獲取當前fiber節點對應的hook,并通過hook.memoizedState獲取緩存的callback和deps
  • 當依賴存在時,通過areHookInputsEqual判斷deps是否變化,如果沒變則返回緩存中的callback,即prevState[0],否則緩存新的callback和deps,然后返回新的callback

在areHookInputsEqual中主要是通過Object.is來判斷deps是否變化

function areHookInputsEqual(nextDeps, prevDeps) {if (prevDeps === null) {return false;}// 簡單的長度檢查if (nextDeps.length !== prevDeps.length) {return false;}// 逐一比較每一個依賴項for (let i = 0; i < nextDeps.length; i++) {if (Object.is(nextDeps[i], prevDeps[i])) {continue;}return false;}return true;
}

Object.is() 與 == 運算符并不等價。== 運算符在測試相等性之前,會對兩個操作數進行類型轉換(如果它們不是相同的類型),這可能會導致一些非預期的行為,例如 “” == false 的結果是 true,但是 Object.is() 不會對其操作數進行類型轉換。
Object.is() 也不等價于 === 運算符。Object.is() 和 === 之間的唯一區別在于它們處理帶符號的 0 和 NaN 值的時候。=== 運算符(和 == 運算符)將數值 -0 和 +0 視為相等,但是會將 NaN 視為彼此不相等。詳細查看MDN

useMemo

useCallback、useMemo都是處于性能考慮通過緩存來避免重復執行的hook,同useCallback一樣,useMemo也接收兩個參數callback、deps。其區別主要是:useCallback是緩存以及返回函數,并不會調用函數,而useMemo會執行函數,緩存并換回函數的執行結果
同其他hooks一樣,useMemo也分為了mount和update兩個,下面一一介紹。

mountMemo

function mountMemo<T>(nextCreate: () => T,deps: Array<mixed> | void | null,
): T {// 創建一個添加到Fiber節點上的Hooks鏈表const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;// 計算需要memo的值const nextValue = nextCreate();// hook數據對象上存的值hook.memoizedState = [nextValue, nextDeps];return nextValue;
}

初次渲染:

  • mountWorkInProgressHook: 會創建一個hook鏈表并綁定到當前渲染的fiber中
  • 執行傳入的callback,并將其保存到memoizedState中

updateMemo

function updateMemo<T>(nextCreate: () => T,deps: Array<mixed> | void | null,
): T {// 找到該useMemo對應的hook數據對象const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;// 之前存的[nextValue, nextDeps]const prevState = hook.memoizedState;if (prevState !== null) {if (nextDeps !== null) {const prevDeps: Array<mixed> | null = prevState[1];// 判斷依賴是否相等if (areHookInputsEqual(nextDeps, prevDeps)) {// 相等就返回上次的值return prevState[0];}}}// 不相等重新計算const nextValue = nextCreate();hook.memoizedState = [nextValue, nextDeps];return nextValue;
}

更新渲染:

  • 通過updateWorkInProgressHook獲取當渲染fiber的hook鏈表
  • 根據areHookInputsEqual判斷傳入的依賴deps是否變化,如果變化則返回新的結果并緩存,否則使用緩存

總結

總的來說useMemo和useCallback相對來說源碼比較簡單,大致就是在首次渲染時,調用mountHook將callback/結果緩存到當前fiber節點的hoos鏈表(通過mountWorkInProgressHook創建)的memoizedState屬性中,然后在更新渲染中獲取當前fiber節點的hook信息(通過updateWorkInProgressHook獲取),通過areHookInputsEqual判斷是否使用緩存。

函數調用流程如下:
在這里插入圖片描述
雖然useCallback、useMemo利用緩存避免了重復渲染,有利于性能優化,但是在實際項目中并不是所有的函數都需要用其包裹,大多情況下是沒有意義的。主要場景就是上面提到的子組件更新和作為其他函數的依賴時:

  • 將其作為 props 傳遞給包裝在 [memo] 中的組件。如果 props 未更改,則希望跳過重新渲染。緩存允許組件僅在依賴項更改時重新渲染。
  • 傳遞的函數可能作為某些 Hook 的依賴。比如,另一個包裹在 useCallback 中的函數依賴于它,或者依賴于 useEffect 中的函數。

當然如果能接受所有函數都被其包裹導致的代碼可讀性問題,這樣記憶化處理也不會有什么問題。

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

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

相關文章

使用selenium定位input標簽下的下拉框

先來看一下頁面效果&#xff1a;是一個可輸入的下拉列表 再來看一下下拉框的實現方式&#xff1a; 是用<ul>和<li>方式來實現的下拉框&#xff0c;不是select類型的&#xff0c;所以不能用傳統的select定位方法。 在著手定位元素前一定一定要先弄清楚下拉列表…

前后端的學習框架

前后端的學習框架 視頻鏈接&#xff1a;零基礎AI全棧開發系列教程&#xff08;一&#xff09;_嗶哩嗶哩_bilibili

什么是后端?

1、什么是后端&#xff1f; 后端開發人員從事于構建Web應用程序背后的實際邏輯&#xff0c;負責通過API向前端或者其他系統提供其他需要的信息&#xff0c;如&#xff1a;數據。 實際上&#xff0c;開發web應用中對用戶不可見的部分&#xff0c;稱為web后端&#xff0c;也就是…

初學vue3與ts:獲取組件ref實例

/*** 獲取組件ref* param {VueComponentIns} 組件實例* returns 組件ref*/ // eslint-disable-next-line export function useCompRef<T extends abstract new (...args: any) > any>(_: T) {return ref<InstanceType<T>>(); }使用 <a-com ref"a…

匯凱金業:數字貨幣對經濟的影響有哪些

隨著信息技術的飛速發展&#xff0c;數字貨幣作為一種新興的貨幣形態&#xff0c;正逐步走進人們的視野&#xff0c;并對傳統經濟體系產生著深遠影響。它不僅革新了交易方式&#xff0c;更在重塑金融格局、賦能經濟發展等方面展現出巨大潛力。 一、交易效率的“加速器” 數字…

單例模式之懶漢式

文章目錄 單例模式&#xff08;懶漢式&#xff09;代碼懶漢式&#xff08;線程不安全&#xff09;懶漢式&#xff08;線程安全&#xff0c;加鎖&#xff09;雙重檢查鎖&#xff08;線程安全&#xff0c;推薦&#xff09; 單例模式&#xff08;懶漢式&#xff09; 懶漢式是符合…

xxl-job集成SpringBoot

安裝xxl-job客戶端一般有很多方式&#xff0c;我這里給大家提供兩種安裝方式&#xff0c;包含里面的各項配置等等。 前期需要準備好MySQL數據庫。復制SQL到數據庫里面。 # # XXL-JOB v2.4.2-SNAPSHOT # Copyright (c) 2015-present, xuxueli.CREATE database if NOT EXISTS x…

項目機會:4萬平:智能倉,AGV,穿梭車,AMR,WMS,提升機,機器人……

導語 大家好&#xff0c;我是社長&#xff0c;老K。專注分享智能制造和智能倉儲物流等內容。 如下為近期國內智能倉儲物流相關項目的公開信息線索&#xff0c;這些項目具體信息會發布到知識星球&#xff0c;請感興趣的球友先人一步到知識星球【智能倉儲物流技術研習社】自行下載…

《SoC設計方法與實現》:全面掌握系統芯片設計精髓(可下載)

SoC&#xff08;System on Chip&#xff0c;系統級芯片&#xff09;設計是一項復雜而精細的工程活動&#xff0c;它涉及到將一個完整的電子系統的所有組件集成到一個單一的芯片上&#xff0c;包括處理器核心、內存、輸入/輸出端口以及可能的其他功能模塊。這種集成不僅要求設計…

oracle存儲結構-----邏輯存儲結構(表空間、段、區、塊)

文章目錄 oracle存儲結構圖&#xff08;邏輯存儲物理存儲&#xff09;oracle邏輯存儲結構圖邏輯存儲結構、表空間、段、區、數據塊的關系&#xff1a;1、數據 塊&#xff08;block&#xff09;---邏輯存儲最小單位2、 數據區&#xff08;extent&#xff09;--存儲空間分配和回收…

【AutoencoderKL】基于stable-diffusion-v1.4的vae對圖像重構

模型地址&#xff1a;https://huggingface.co/CompVis/stable-diffusion-v1-4/tree/main/vae 主要參考:Using-Stable-Diffusion-VAE-to-encode-satellite-images sd1.4 vae 下載到本地 from diffusers import AutoencoderKL from PIL import Image import torch import to…

電腦經常黑屏

情況簡述&#xff1a; 電腦經常突然黑屏&#xff0c;并且鼠標還能看到并且可操控 你是不是試過以下方法&#xff1a; 更換顯卡驅動版本?重置BIOS?重裝系統?全網找千篇一律沒啥用的教程? 這個標志熟悉吧&#xff0c;看看你的電腦里是否安裝了火絨&#xff0c;如果裝了繼續…

MySQL在Windows系統上的詳細安裝指南

一、準備工作 1. 確定MySQL版本 訪問MySQL官方網站&#xff1a;首先&#xff0c;你需要訪問MySQL的官方網站&#xff08;MySQL&#xff09;&#xff0c;或者更具體地&#xff0c;訪問MySQL的下載頁面&#xff08;MySQL :: Download MySQL Community Server&#xff09;。 選擇…

Linux運維:mysql主從復制原理及實驗

當一臺數據庫服務器出現負載的情況下&#xff0c;需要擴展服務器服務器性能擴展方式有向上擴展&#xff0c;垂直擴展。向外擴展&#xff0c;橫向擴展。通俗的講垂直擴展是將一臺服務器擴展為性能更強的服務器。橫向擴展是增加幾臺服務器。 主從復制好比存了1000塊錢在主上&…

Android14之獲取包名/類名/服務名(二百二十三)

簡介&#xff1a; CSDN博客專家&#xff0c;專注Android/Linux系統&#xff0c;分享多mic語音方案、音視頻、編解碼等技術&#xff0c;與大家一起成長&#xff01; 優質專欄&#xff1a;Audio工程師進階系列【原創干貨持續更新中……】&#x1f680; 優質專欄&#xff1a;多媒…

深度學習-梯度下降算法-NLP(五)

梯度下降算法 深度學習中梯度下降算法簡介找極小值問題數學上求最小值梯度梯度下降算法 找極小值問題在深度學習流程中深度學習整體流程圖求解損失函數的目標權重的更新 深度學習中梯度下降算法簡介 找極小值問題 引子&#xff1a; 我們訓練一個人工智能模型&#xff0c;簡單…

磁致伸縮液位計原理和特點

工作原理 磁致伸縮液位計的工作原理基于磁性材料在外部磁場作用下的尺寸變化來進行液位測量。該液位計主要由電子變送器、浮球&#xff08;浮子&#xff09;、探測桿&#xff08;測桿&#xff09;三部分組成。在磁致伸縮液位計的傳感器測桿外配有一浮子&#xff0c;此浮子可以…

【SpringCloud應用框架】Nacos服務配置中心

第四章 Spring Cloud Alibaba Nacos之服務配置中心 文章目錄 一、基礎配置二、新建子項目1.pom文件2.YML配置3.啟動類4.業務類5.Nacos配置規則 三、Nacos平臺創建配置操作四、自動配置更新五、測試 一、基礎配置 Nacos不僅僅可以作為注冊中心來使用&#xff0c;同時它支持作為…

【環境準備】 Vue環境搭建

文章目錄 前言vue-cli 安裝創建項目3.0、以下3.0 、以上 前言 書接上回《NodeJs(壓縮包版本)安裝與配置》&#xff0c;安裝完了NodeJs&#xff0c;接下來就要配置vue的環境了。 vue-cli 安裝 安裝vue-cli輸入如下命令 #&#xff08;安裝的是最新版&#xff09; npm install …

觀察者模式(Observer Pattern)

觀察者模式&#xff08;Observer Pattern&#xff09; 定義 觀察者模式定義了一種一對多的依賴關系&#xff0c;讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時&#xff0c;會通知所有觀察者對象&#xff0c;使它們能夠自動更新自己。別名&#xff1…