重學React(五):脫圍機制一

背景: 之前將React的基礎知識以及狀態管理相關的知識都過了一遍,查漏補缺的同時對React也有了一些新鮮的認知,接下來這個模塊的名字很有意思:脫圍機制,內容也比之前的部分難理解一些。但整體看下來,理解之后對React的使用上也會更上一層樓。就繼續學習吧~

前期回顧:
重學React(一):描述UI
重學React(二):添加交互
重學React(三):狀態管理
重學React(四):狀態管理二

學習內容:

React官網教程:https://zh-hans.react.dev/learn/escape-hatches
其他輔助資料(看到再補充)
補充說明:這次學習更多的是以學習筆記的形式記錄,看到哪記到哪

什么是脫圍機制
在React中,除了React之外,我們還需要連接外部系統,比如需要連接服務器接口,獲取服務器傳來的數據,再比如操作DOM方法,比如focus,scroll等等。這些功能的前提需要“跳出”React自身的渲染邏輯,所以被稱為脫圍機制。接下來就開始學習如何脫圍吧~

使用 ref 引用值

在實際編碼中,偶爾會遇到希望組件能記住某些信息,這些信息的修改不觸發頁面重新渲染,比如記錄setTimeout的id,這個id本身跟渲染毫無關系,只是用來標識當前的計時器以及在卸載組件時銷毀它,如果不記錄下來,就很難實現銷毀,容易造成內存泄漏,此時就需要使用ref

給組件添加ref
import { useRef } from 'react';export const App () {
// useRef返回一個current對象
// {  current: 0 } // current的value是向 useRef 傳入的值,任何類型都可以const ref = useRef(0);	
}

在這里插入圖片描述
可以使用ref.current 屬性訪問該 ref 的當前值。ref 是一個普通的 JavaScript 對象,具有可以被讀取和修改的 current 屬性。
這個值是有意被設置為可變的,意味著既可以讀取它也可以寫入它。就像一個 React 追蹤不到的、用來存儲組件信息的秘密“口袋”。

示例:制作秒表
import { useState, useRef } from 'react';export default function Stopwatch() {
// 記錄開始時間和當前時間,因為這兩個時間需要計算并渲染出最后的結果,所以使用state,實現實時渲染const [startTime, setStartTime] = useState(null);const [now, setNow] = useState(null);// 用來記錄當前計時器的id,便于重置時clearInterval,它在頁面重新渲染時不需要改變,而是在進行操作時手動處理,所以使用ref進行記錄const intervalRef = useRef(null);
// 每次點擊開始時,將當前時間和記錄時間重置function handleStart() {setStartTime(Date.now());setNow(Date.now());clearInterval(intervalRef.current);intervalRef.current = setInterval(() => {// 每隔十秒更新當前時間setNow(Date.now());}, 10);}function handleStop() {clearInterval(intervalRef.current);}let secondsPassed = 0;// 每次渲染用當前時間減去開始時間,就能得到過去了多少時間if (startTime != null && now != null) {secondsPassed = (now - startTime) / 1000;}return (<><h1>時間過去了: {secondsPassed.toFixed(3)}</h1><button onClick={handleStart}>開始</button><button onClick={handleStop}>停止</button></>);
}

ref 和 state 的不同之處

在這里插入圖片描述

// React 內部,useRef的內部運行機制可以簡單由useState實現
// 第一次渲染期間,useRef 返回 { current: initialValue }。 該對象由 React 存儲,因此在下一次渲染期間將返回相同的對象。 
// 在這個示例中,state 設置函數沒有被用到。它是不必要的,因為 useRef 總是需要返回相同的對象!
function useRef(initialValue) {const [ref, unused] = useState({ current: initialValue });return ref;
}
ref使用場景
  • 存儲 timeout ID
  • 存儲和操作 DOM 元素
  • 存儲不需要被用來計算 JSX 的其他對象。
    總的來說,如果組件需要存儲一些值,但不影響渲染邏輯,請選擇 ref,這通常是不會影響組件外觀的瀏覽器 API。
ref 的最佳實踐

使用ref的原則

  • 將 ref 視為脫圍機制。 在使用外部系統或瀏覽器 API 時,ref 很有用。但如果很大一部分應用程序邏輯和數據流都依賴于 ref,可能需要重新考慮方法是否有問題。
  • 不要在渲染過程中讀取或寫入 ref.current。 如果渲染過程中需要某些信息,請使用 state 代替。由于 React 不知道 ref.current 何時發生變化,即使在渲染時讀取它也會使組件的行為難以預測。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 這樣的代碼,它只在第一次渲染期間設置一次 ref。)
    ref本身就是一個普通的js對象,所以它的數據會實時更新,不會像state一樣以快照的形式每隔一段時間才更新。所以只要ref的值不涉及渲染,React就不會關心你對 ref 或其內容做了什么。

使用Ref操作DOM

這是ref最常見的使用場景。在大部分情況下,React 會自動處理更新 DOM 以匹配渲染輸出,所以不需要操作DOM。但在實現某些效果的情況下,比如控制DOM的滾動,讓某個元素獲得焦點等等,React沒有內置方法,而是需要一個指向 DOM 節點的 ref 來實現。
接下來是具體的實現以及原理:

使文本輸入框獲得焦點
// 引入hook
import { useRef } from 'react';export default function Form() {
// 聲明一個refconst inputRef = useRef(null);function handleClick() {// inputRef.current中保存的就是input節點,可以直接使用這個節點內置的API,這里使用的是focusinputRef.current.focus();}return (<>// 將 ref 作為 ref 屬性值傳遞給想要獲取的 DOM 節點的 JSX 標簽<input ref={inputRef} /><button onClick={handleClick}>聚焦輸入框</button></>);
}
如何使用 ref 回調管理 ref 列表

考慮一個場景:有n個列表,需要給每個列表都綁定一個ref,n的個數是未知的,所以我們不能預先將ref給一一聲明了,因為 Hook 只能在組件的頂層被調用。所以不能在循環語句、條件語句或 map() 函數中調用 useRef 。解決這個問題有兩種思路:

  1. 用一個 ref 引用其父元素,然后用 DOM 操作方法如 querySelectorAll 來尋找它的子節點。然而,這種方法很脆弱,如果 DOM 結構發生變化,可能會失效或報錯
  2. ref 回調,也就是將函數傳遞給 ref 屬性。當需要設置 ref 時,React 將傳入 DOM 節點來調用 ref 回調,并在需要清除它時傳入 null 。這可以維護自己的數組或 Map,并通過其索引或某種類型的 ID 訪問任何 ref
    看個例子如何用第二個方法來解決問題:
    注意事項:啟用嚴格模式后,ref 回調將在開發中運行兩次
import { useRef, useState } from "react";export default function CatFriends() {const itemsRef = useRef(null);const [catList, setCatList] = useState(setupCatList);function scrollToCat(cat) {const map = getMap();const node = map.get(cat);node.scrollIntoView({behavior: "smooth",block: "nearest",inline: "center",});}function getMap() {if (!itemsRef.current) {// 首次運行時初始化 Map。itemsRef.current = new Map();}return itemsRef.current;}return (<><nav><button onClick={() => scrollToCat(catList[0])}>Neo</button><button onClick={() => scrollToCat(catList[5])}>Millie</button><button onClick={() => scrollToCat(catList[9])}>Bella</button></nav><div><ul>{catList.map((cat) => (<likey={cat}ref={(node) => {// 將這個getMap函數傳入,這樣DOM的ref就可以以map的形式操作const map = getMap();// 添加到 Map 中map.set(cat, node);// 從 Map 中移除return () => {map.delete(cat);};}}><img src={cat} /></li>))}</ul></div></>);
}function setupCatList() {const catList = [];for (let i = 0; i < 10; i++) {catList.push("https://loremflickr.com/320/240/cat?lock=" + i);}return catList;
}
訪問另一個組件的 DOM 節點

有時候會有A組件操作B組件DOM節點的需求,比如在執行某些操作后,實現表單輸入框的自動聚焦。但Ref 是一個脫圍機制,也就是除了在迫不得已的情況下盡量別用。手動操作其它 組件的 DOM 節點可能會讓代碼變得脆弱。如果真的要用,可以看看這個例子。

import { useRef } from 'react';function MyInput({ ref }) {
// 子組件從props中獲取ref,綁定在對應的DOM節點上return <input ref={ref} />;
}export default function MyForm() {
// 在父組件里聲明refconst inputRef = useRef(null);function handleClick() {inputRef.current.focus();}return (<>// 把ref作為參數傳到子組件中<MyInput ref={inputRef} /><button onClick={handleClick}>聚焦輸入框</button></>);
}

這樣做確實可以實現在A組件中調用B組件的DOM,但某些情況下,可能只需要調用B組件DOM的其中一些方法,比如在這個例子里只需要調用focus方法,但這樣寫會將DOM所有方法都給了MyForm組件。還有些更加極端的需求,A組件可能需要調用B組件中的某些方法,這個時候,可以使用useImperativeHandle來實現

import { useRef, useImperativeHandle } from "react";function MyInput({ ref }) {const realInputRef = useRef(null);// useImperativeHandle 指示 React 將你自己指定的對象作為父組件的 ref 值。 // 所以 Form 組件內的 inputRef.current 將只有 focus 方法。useImperativeHandle(ref, () => ({// 只暴露 focus,沒有別的// ref在這里不是 DOM 節點,而是在 useImperativeHandle 調用中創建的自定義對象。所以除了DOM方法外,還可以將其他A組件需要調用的方法也一并傳入focus() {realInputRef.current.focus();},someFun() {console.log('test')}}));return <input ref={realInputRef} />;
};export default function Form() {const inputRef = useRef(null);function handleClick() {inputRef.current.focus();}return (<><MyInput ref={inputRef} /><button onClick={handleClick}>聚焦輸入框</button></>);
}
React 何時添加 refs

在 React 中,每次更新都分為 兩個階段:

  • 在 渲染 階段, React 調用你的組件來確定屏幕上應該顯示什么。
  • 在 提交 階段, React 把變更應用于 DOM。
    在第一次渲染期間,DOM 節點尚未創建,因此 ref.current 將為 null。在渲染更新的過程中,DOM 節點還沒有更新。所以讀取它們還為時過早。
    React 在提交階段設置 ref.current。在更新 DOM 之前,React 將受影響的 ref.current 值設置為 null。更新 DOM 后,React 立即將它們設置到相應的 DOM 節點。
    通常,你將從事件處理器訪問 refs。 如果想使用 ref 執行某些操作,但沒有特定的事件可以執行此操作,可能需要一個 effect。這就是后面的內容了。

彩蛋:用 flushSync 同步更新 state
請看下面這個代碼,需要實現的是添加一個新的待辦事項,并將屏幕向下滾動到列表的最后一個子項。請注意,出于某種原因,它總是滾動到最后一個添加之前的待辦事項

import { useState, useRef } from 'react';export default function TodoList() {const listRef = useRef(null);const [text, setText] = useState('');const [todos, setTodos] = useState(initialTodos);function handleAdd() {const newTodo = { id: nextId++, text: text };setText('');setTodos([ ...todos, newTodo]);listRef.current.lastChild.scrollIntoView({behavior: 'smooth',block: 'nearest'});}return (<><button onClick={handleAdd}>添加</button><inputvalue={text}onChange={e => setText(e.target.value)}/><ul ref={listRef}>{todos.map(todo => (<li key={todo.id}>{todo.text}</li>))}</ul></>);
}let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {initialTodos.push({id: nextId++,text: '待辦 #' + (i + 1)});
}

執行代碼后會發現,原本想要滾動到最后新加的待辦事項中,但實際上會滾到上一個事項,自動滾動無法定位到新添加的待辦事項中。
問題出現在這兩行代碼中:

// 在 React 中,state 更新是排隊進行的,setTodos 不會立即更新 DOM。
// 當ref操作scroll事件使得列表滾動到最后一個元素時,尚未添加待辦事項
// 因此這里需要實現setTodos立即更新
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();// 可以使用react-dom中的flushSync來實現這個強制更新DOM的過程
import { flushSync } from 'react-dom';
// ...只展示關鍵代碼
flushSync(() => {setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
使用 refs 操作 DOM 的最佳實踐

還是反復強調的事情,Ref是一種脫圍機制,所以必須只在需要跳出“React”范圍的時候才能使用,否則如果胡亂修改DOM元素,一旦跟React自身的渲染機制沖突了,就容易造成不可預期的后果。
因此,需要避免更改由 React 管理的 DOM 節點。 對 React 管理的元素進行修改、添加子元素、從中刪除子元素會導致不一致的視覺結果,或造成代碼崩潰。總之就是,不是不能改,而是改的時候需要小心些。

ref的場景就學完了,接下來是Effect的模塊,這個模塊比較長,就單獨再開一篇來講好了~

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

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

相關文章

去除Edge微軟瀏覽器與Chrome谷歌瀏覽器頂部出現“此版本的Windows不再支持升級Windows 10”的煩人提示

前言 在 Windows 7 中&#xff0c;安裝 Microsoft Edge 109 版本后&#xff0c;啟動瀏覽器時會彈出提示&#xff1a; 此版本的 Windows 不再支持 Microsoft Edge。升級到 Windows 10 或更高版本&#xff0c;以獲取常規功能和安全更新。 同樣地&#xff0c;安裝 Google Chrome 1…

PWM、脈沖

要求&#xff1a;一、PWM輸出PWM波生成原理在此處使用TIM2生成PWM&#xff0c;PA1輸出PWM波。CNT小于CCR時&#xff0c;輸出高電平&#xff1b;CNT大于CCR時&#xff0c;輸出低電平。 輸入捕獲測量頻率的原理輸入捕獲的捕獲意思是它在PWM波上升沿或者下降沿的時候&#xff0c;會…

文件IO(1)

.文件IO1.概念標準IO是有緩存的IO&#xff0c;文件IO沒有緩存&#xff0c;適合于通信、硬件設備操作標準IO是庫函數&#xff0c;文件IO是系統調用2.系統調用與庫函數系統調用&#xff1a;是Linux內核中的代碼&#xff0c;只能在Linux系統中使用庫函數&#xff1a;是對系統調用的…

【AI】Pycharm中要注意Python程序文件的位置

博主試著在本地電腦用Pycharm環境運行隨便一個機器學習然后做圖像識別的模型&#xff0c;Python的程序一直報博主學習圖片的路徑不正確&#xff0c;博主查了好幾遍&#xff0c;也沒找出問題&#xff0c;后來借助Deepseek才知道&#xff0c;Python主程序的位置一定要在Project下…

TDengine 可觀測性最佳實踐

TDengine 介紹 TDengine 是一款開源、高性能、云原生的時序數據庫&#xff0c;專為物聯網、車聯網、工業互聯網、金融、IT 運維等場景優化設計。它不僅提供了高效的數據存儲和查詢功能&#xff0c;還帶有內建的緩存、流式計算、數據訂閱等系統功能&#xff0c;能大幅減少系統設…

Jenkins 搭建鴻蒙打包

1、創建流水線工程 選擇 Freestyle project 2、配置模板倉庫、憑證 配置倉庫地址 創建憑證&#xff0c;憑證選擇賬號-密碼&#xff08;能夠訪問該倉庫的個人或管理員 Gitlab 賬密&#xff09; 到這里執行構建&#xff0c;便可以克隆倉庫到工作目錄 3、安裝插件 3.1 Rebuild…

【SpringBoot】02 基礎入門-什么是Spring Boot?:Spring與SpringBoot

文章目錄1、Spring能做什么1.1、Spring的能力1.2、Spring的生態1.3、Spring5重大升級1.3.1、響應式編程1.3.2、內部源碼設計2、為什么用SpringBoot2.1、SpringBoot優點2.2、SpringBoot缺點3、時代背景3.2、分布式分布式的困難分布式的解決3.3、云原生上云的困難4、如何學習Spri…

FFmpeg 編譯安裝和靜態安裝

FFmpeg 編譯安裝和靜態安裝 簡介 FFmpeg 是一個領先的多媒體框架&#xff0c;能夠解碼、編碼、轉碼、復用、解復用、流化、過濾和播放幾乎所有人類和機器創建的格式。本指南將詳細介紹如何在 CentOS 8.5.2111 系統上從源代碼編譯并安裝 FFmpeg 6.1.1 版本。從源代碼編譯安裝可…

人大BABEC地平線高效率具身導航!Aux-Think:探索視覺語言導航中數據高效的推理策略

作者&#xff1a; Shuo Wang1,3^{1,3}1,3, Yongcai Wang1^{1}1, Wanting Li1^{1}1 , Xudong Cai1^{1}1, Yucheng Wang3^{3}3, Maiyue Chen3^{3}3, Kaihui Wang3^{3}3, Zhizhong Su3^{3}3, Deying Li1^{1}1, Zhaoxin Fan2^{2}2單位&#xff1a;1^{1}1中國人民大學&#xff0c;2^…

01. maven的下載與配置

1.maven的下載與初步配置a.下載并配置倉庫地址下載maven壓縮包&#xff0c;并解壓&#xff0c;解壓后應有如下幾個文件點擊conf&#xff0c;打開settings.xml&#xff08;我用的VScode打開的&#xff09;&#xff0c;我們需要聲明一下內部倉庫的地址&#xff0c;以及私服的一些…

1701. 請輸出所有的3位對稱數

問題描述請輸出所有的 33 位對稱數&#xff0c;對稱數指的是一個整數 nn 正過來和倒過來是一樣的&#xff0c;比如&#xff1a;101、121、282…101、121、282…請從小到大輸出符合條件的3位對稱數&#xff0c;每行 11 個。輸入無。輸出從小到大按題意輸出符合條件的數&#xff…

C++算法·排序

排序的定義 這個不用說吧 就是根據某個條件對一個數列進行有序的操作 例如要求從小到大排序、從大到小排序等等 排序的分類 比較排序(Comparison(Comparison(Comparison Sorts)Sorts)Sorts) 特點&#xff1a;通過元素間的比較決定順序 時間復雜度下限&#xff1a;O(nO(nO(n…

微服務項目中的注冊中心——Nacos配置

從零開始&#xff1a;Nacos服務注冊與配置中心實戰教程 Nacos&#xff08;Dynamic Naming and Configuration Service&#xff09;是阿里巴巴開源的服務發現、配置管理工具&#xff0c;集注冊中心與配置中心于一體&#xff0c;廣泛應用于微服務架構。本文將從環境搭建到實戰配…

日期格式化成英文月,必須指定語言環境

如果不指定Locale.ENGLISH 在有些JDK下 輸出6月 INV USD 314,791.77,DUE 25-07 [PAID USD 503,389.56 ON 2025-07-16]Mar INV USD 52,042.00,DUE 25-07 [PAID USD 52,042.00 ON 2025-08-11]所以必…

【6】Transformers快速入門:Transformer 的注意力層 是啥?

一句話看懂注意力層作用&#xff1a;讓 AI 像人一樣 “抓重點” &#xff08;比如讀“貓追老鼠”&#xff0c;自動聚焦 “追” 這個動作&#xff0c;忽略無關詞&#xff09;1. 為什么需要注意力&#xff1f; 問題場景&#xff08;翻譯例子&#xff09;&#xff1a; 英文&#x…

集合,完整擴展

目錄 前言&#xff1a; 一、List接口 1.1 ArrayList 1.2 LinkedList 1.3 Vector 二、Set接口 2.1 HashSet 2.2 TreeSet 2.3 LinkedHashSet 三、應用選擇 前言&#xff1a; 本篇文章重點梳理 List 接口和 Set 接口的核心內容&#xff0c;結合代碼案例幫大家吃透它們的…

【doris基礎與進階】3-Doris安裝與部署

安裝前的準備 在windows系統上通過vmwareubuntu 22.04的方式進行安裝&#xff0c;由于資源有限&#xff0c;在同1臺機器上同時安裝fe和be&#xff08;broker本次不安裝&#xff0c;極簡化安裝&#xff09;&#xff0c;安裝版本為2.1.10&#xff0c;2.x版本架構不會有大的變化&a…

關于數據結構6-哈希表和5種排序算法

哈希表1哈希算法將數據通過哈希算法映射成一個鍵值&#xff0c;存取都在同一個位置實現數據的高效存儲和查找&#xff0c;將時間復雜度盡可能降低至O(1)2哈希碰撞多個數據通過哈希算法得到的鍵值相同&#xff0c;成為產生哈希碰撞3哈希表&#xff1a;構建哈希表存放0-100之間的…

AWT與Swing深度對比:架構差異、遷移實戰與性能優化

全面對比分析Java AWT與Swing GUI框架的架構差異、性能表現和適用場景&#xff0c;提供完整的AWT到Swing遷移實戰指南&#xff0c;包含15代碼示例、性能測試數據、最佳實踐建議&#xff0c;助你做出明智的技術選型和實現平滑遷移。 Java AWT, Swing, GUI框架對比, 代碼遷移, 性…

git倉庫檢測工具

介紹 Gitleaks 是一款用于檢測git 倉庫、文件以及任何你想通過 git 傳遞的信息(例如密碼、API 密鑰和令牌)的工具stdin。如果你想了解更多關于檢測引擎工作原理的信息,請查看這篇博客:正則表達式(幾乎)就是你所需要的一切。 ? ~/code(master) gitleaks git -v○│╲│…