React中Refs的使用場(chǎng)景及核心要點(diǎn)詳解
在使用 React 進(jìn)行開(kāi)發(fā)過(guò)程中,或多或少使用過(guò) Refs 進(jìn)行 DOM 操作或者訪問(wèn)一些DOM上的API,又或使用 Refs 保存數(shù)據(jù)。不管怎么說(shuō) Refs 總是 React 提供的一大助力,這篇文章主要介紹 Refs 功能和使用場(chǎng)景以及注意事項(xiàng)。希望能增強(qiáng)對(duì) Refs 的理解,掌握好這把利劍。
什么是 Refs
Refs 是 React 提供的用來(lái)保存 object 引用的一個(gè)解決方案,在函數(shù)式組件使用 useRef 創(chuàng)建一個(gè) ref 對(duì)象,ref 對(duì)象存在一個(gè)可直接修改的 current 屬性,內(nèi)容都是存在 current 上。Refs 使用場(chǎng)景主要分為兩個(gè)方向,其一是實(shí)現(xiàn) DOM 訪問(wèn)與操控、在兩次render之間傳遞數(shù)據(jù)內(nèi)容【和state機(jī)制有很大不同,下文會(huì)有對(duì)比介紹】。如果在組件返回的 jsx dom上綁定了 ref 屬性,React 在處理 jsx 時(shí)會(huì)把該dom節(jié)點(diǎn)【原生node節(jié)點(diǎn)】的引用存儲(chǔ)在 ref.current 上。
使用方式
分為三步:
- 第一步、使用 useRef 創(chuàng)建 ref 對(duì)象(useRef 是 FC hooks, class 組件使用
React.createRef()
創(chuàng)建 ) - 第二步、賦值&使用【操作dom則綁定為dom的ref屬性的值,用于保存值的時(shí)候傳遞內(nèi)容給 ref.current】,
- 第三步、訪問(wèn)ref內(nèi)容【進(jìn)行dom對(duì)應(yīng)的api訪問(wèn),進(jìn)行 scroll 、focus等操作。又或者從current中讀取保存的數(shù)據(jù)】最終的目的還是最后訪問(wèn)拿到對(duì)應(yīng)的數(shù)據(jù)進(jìn)行操作。下邊我們分別用兩個(gè)小 demo 簡(jiǎn)單先看看用法,理論和總結(jié)在后邊一點(diǎn)【熟悉Refs使用的直接跳轉(zhuǎn): 核心要點(diǎn)】。
Example one 實(shí)現(xiàn)點(diǎn)擊按鈕 focus input 框
import React, { useRef } from "react"; export default function Comp() { // 第一步:使用 useRef 創(chuàng)建一個(gè) ref 對(duì)象 { current: null } const ref = useRef(); function handleClick() { // 第三步:訪問(wèn)到 ref 上存的內(nèi)容,這里是 input 的node節(jié)點(diǎn) ref.current.focus(); } // 第二步:賦值 ref return ( <> <input ref={ref} /> <button onClick={handleClick}>開(kāi)始輸入</button> </> ); }
Example two 實(shí)現(xiàn)數(shù)據(jù)發(fā)送3s內(nèi)撤回功能:在點(diǎn)擊發(fā)送后3s內(nèi)如果點(diǎn)擊 “取消發(fā)送” 則取消本次發(fā)送
簡(jiǎn)單起見(jiàn)我們按鈕不實(shí)際發(fā)送請(qǐng)求,定時(shí) 3s 如果3s內(nèi)點(diǎn)擊了 “取消發(fā)送”則取消發(fā)送。發(fā)送功能用提醒 “已發(fā)送”代替,出現(xiàn)“已發(fā)送”表示執(zhí)行了發(fā)送。
import React, { useRef, useState } from "react"; export default function CompA() { // 第一步:使用useRef 創(chuàng)建 ref 對(duì)象 const ref = useRef(); const [isSending, setIsSending] = useState(false); function send() { // ... window.alert("消息已發(fā)送!"); setIsSending(false); } function undo() { // 第三步: 訪問(wèn)存在 ref 上的 timeout ID, 進(jìn)行定時(shí)取消 clearTimeout(ref.current); } function handleClickSendBtn() { setIsSending(true); // 第二步: 賦值,將 timeout ID 存在 ref 上 ref.current = setTimeout(send, 3000); } function handleClickCancelBtn() { undo(); setIsSending(false); } return ( <> <button onClick={handleClickSendBtn} disabled={isSending}> {isSending ? "發(fā)送中..." : "發(fā)送"} </button> {isSending && <button onClick={handleClickCancelBtn}>取消發(fā)送</button>} </> ); }
Refs 核心要點(diǎn)
我們通過(guò)兩個(gè)簡(jiǎn)單 case 演示了一下,DOM 操作 以及用于在兩次 re-render 之間傳遞內(nèi)容(case 2 傳遞的內(nèi)容是 timeout 的ID)。在使用 Refs 的過(guò)程中有幾點(diǎn)尤其需要注意。
避免重復(fù)創(chuàng)建 ref 內(nèi)容
在使用 useRef 進(jìn)行創(chuàng)建 ref 時(shí)可以傳遞 null、number 、object 等內(nèi)容也可以傳遞初始化函數(shù)。React 只會(huì)保存一次初始值,并把它帶到下一次 render 中。因此在 useRef 在創(chuàng)建ref的時(shí)候傳遞重復(fù)的內(nèi)容是不生效的,如果你認(rèn)為每次都生成一個(gè)新的值賦給ref但是React給你的卻是第一次傳遞的值,這可能不符合你的預(yù)期。
import React, { useEffect, useRef, useState } from "react"; export function CompB() { // 注意: 這個(gè)部分不會(huì)每次生成一個(gè)新的時(shí)間戳,只會(huì)采用 mounted 時(shí)新建的第一次時(shí)間戳。 const ref = useRef(+new Date()); const [count, setCount] = useState(0); useEffect(() => { console.log(`第${count}渲染時(shí)間:`, +new Date()); console.log("ref", ref.current); }); function handleClick() { // 為了讓點(diǎn)擊時(shí),更新 state 觸發(fā) re-render setCount(count + 1); } return ( <> <button onClick={handleClick}>點(diǎn)擊讓組件渲染</button> </> ); }
注意:效果圖中初始化的時(shí)候會(huì)打印兩次重復(fù)的第0次渲染,是因?yàn)?React 在 dev 模式下會(huì)執(zhí)行兩遍組件內(nèi)容,檢測(cè)組件是否是純組件。并非代碼問(wèn)題,后續(xù)研讀源碼時(shí)會(huì)有文章介紹,歡迎關(guān)注。
ref.current 存儲(chǔ)的內(nèi)容修改是突變
對(duì)于 state 來(lái)說(shuō),直接修改state不會(huì)生效。需要使用 useState 給的第二個(gè)返回值來(lái)進(jìn)行修改。而 ref 則是可直接修改 current 屬性上的內(nèi)容,并且修改后可以立即取到值。ref存儲(chǔ)的實(shí)際就是一個(gè)引用,因此是可突變的。
import React, { useRef } from 'react'; export function Comp() { const ref = useRef(0); useEffect(()=>{ console.log(ref.current); // 0 // 突變 ref.current = ref.current + 1; console.log(ref.current); // 1 }); return <div></div> }
ref 作為數(shù)據(jù)存儲(chǔ)時(shí)內(nèi)容的變化不會(huì)引起 re-render
React 組件的 re-render 的觸發(fā)一般是【state、props、context】中的出現(xiàn)變化引起的。修改 Ref 的內(nèi)容不會(huì)引起組件的 re-render 因此不能用 ref 去干預(yù) React 生成jsx。換句話說(shuō)就是不能用在jsx中做渲染或者條件判斷,不然可能得到?jīng)]辦法預(yù)料的jsx結(jié)果。
ref 的讀寫(xiě)只能在 useEffect 或者回調(diào)函數(shù)中進(jìn)行
React 約定 state、props、context 都是一樣的就應(yīng)該輸出同樣的jsx內(nèi)容,只要這三個(gè)要素不變那么以不同的調(diào)用順序執(zhí)行組件應(yīng)該得到同樣的結(jié)果。要說(shuō)清楚為什么Ref的讀寫(xiě)只能在useEffect和回調(diào)函數(shù)中,得先鋪墊一下React的一些架構(gòu)知識(shí)。
React 架構(gòu)上分為三個(gè)部分【調(diào)度器Scheduler、協(xié)調(diào)器Reconciler、渲染器Renderer】,整體上又是兩個(gè)階段【render 階段,commit階段】。render 階段的目的是找出哪些組件需要更新,以及如何更新(這些內(nèi)容會(huì)標(biāo)記在Fiber節(jié)點(diǎn)上)【更新過(guò)程可中斷可搶占的,高優(yōu)的更新可搶占優(yōu)先先執(zhí)行。這個(gè)階段主要是 Scheduler 負(fù)責(zé)調(diào)度優(yōu)先級(jí), 協(xié)調(diào)器負(fù)責(zé)找出更新的內(nèi)容并標(biāo)記好】,commit 階段的作用用一句話就是【根據(jù) render 階段標(biāo)記的結(jié)果Fiber上的tag,操作dom,執(zhí)行 useEffect 以及對(duì)應(yīng)階段的生命周期函數(shù)】。
在 render 階段會(huì)執(zhí)行組件,如果出現(xiàn)高優(yōu)更新?lián)屨迹敲吹蛢?yōu)先級(jí)的更新在高優(yōu)更新執(zhí)行完成后會(huì)重新執(zhí)行一遍【函數(shù)式組件也就是個(gè)function函數(shù),在函數(shù)體中間的執(zhí)行 ref 寫(xiě)操作會(huì)被多次執(zhí)行】,我們會(huì)發(fā)現(xiàn)如果ref的賦值操作在這個(gè)期間執(zhí)行了那么組件更新的結(jié)果就是不可預(yù)期的【未被搶占時(shí)ref的結(jié)果是1,被搶占1次時(shí)是2。這完全是不可預(yù)期大的】。而 useEffect 或者回調(diào)函數(shù)都不是在 render 階段執(zhí)行的因此每次更新只執(zhí)行一次。也就是說(shuō)ref的讀寫(xiě)不能出現(xiàn)在render階段,就只能寫(xiě)在 useEffect【類組件對(duì)應(yīng)的是生命周期函數(shù),注意不能寫(xiě)在 componentWillxxx 生命周期中,因?yàn)?componentWillxxx 生命周期函數(shù)執(zhí)行在 render 階段】和回調(diào)函數(shù)中。
// bad function MyComponent() { // ... // ?? Don't write a ref during rendering myRef.current = 123; // ... // ?? Don't read a ref during rendering return <h1>{myOtherRef.current}</h1>; } // good function MyComponent() { // ... useEffect(() => { // ? You can read or write refs in effects myRef.current = 123; }); // ... function handleClick() { // ? You can read or write refs in event handlers doSomething(myOtherRef.current); } // ... }
跨組件傳遞ref 獲取dom時(shí)需要借助 forwardRef 包裹組件
React 默認(rèn)情況下不允許組件訪問(wèn)其他組件的dom節(jié)點(diǎn),因此關(guān)閉了直接 props 傳遞 ref 標(biāo)記組件的dom這種操作。得借助 React.forwardRef api 傳遞 實(shí)現(xiàn)這種跨組件的dom操作。
import React, { useEffect, useRef, useState, forwardRef } from "react"; export function ParentComp() { const childInputRef = useRef(null); function handleClick() { childInputRef.current?.focus(); } return ( <> <button onClick={handleClick}>編輯</button> <ChildComp ref={childInputRef} /> </> ); } // 使用forwordRef 包裹組件,接受 ref 并轉(zhuǎn)發(fā)綁定到對(duì)應(yīng)dom上 const ChildComp = forwardRef((props, ref) => { return ( <div> <input {...props} ref={ref} /> </div> ); });
ref 綁定的dom在離屏或者未掛載時(shí)ref.current 值會(huì)被修改為null
ref 綁定的dom在離屏或者未掛載時(shí)ref.current 值會(huì)被修改為 null 。如果在組件中間會(huì)進(jìn)行條件渲染,那么需要處理一下判斷邏輯,不然代碼可能會(huì)拋出異常。另外在父組件引用子組件 dom 的場(chǎng)景也應(yīng)該增加對(duì) null 的判斷。至此 Refs 的要點(diǎn)已經(jīng)介紹完成。
最佳實(shí)踐
接下來(lái)我們接著聊聊什么情況下使用 Refs 比較好,React 官方把 Refs 定義為逃生通道,就是暗示要謹(jǐn)慎使用。
dom 操作相關(guān)
- 如果需要進(jìn)行焦點(diǎn)管理、位置滾動(dòng)等非破壞性行為以及調(diào)用 dom 節(jié)點(diǎn)的 api 那么推薦使用 Refs。
- 如果是為了修改 dom ,比如修改dom屬性,標(biāo)簽名稱等等,可能會(huì)與 React 存在沖突,不推薦這樣使用Refs 而是應(yīng)該換種思路考慮使用 state 進(jìn)行條件渲染。
用于在兩次 render 之間傳遞數(shù)據(jù)
- 如果組件中大部分功能都依賴該數(shù)據(jù),那么不應(yīng)該存放在ref中。
- 如果數(shù)據(jù)在jsx中使用,那么不推薦放在ref中, 這會(huì)帶來(lái)問(wèn)題【詳見(jiàn):ref 的讀寫(xiě)只能在 useEffect 或者回調(diào)函數(shù)中進(jìn)行】,推薦使用 useState。
- 想要保存數(shù)據(jù)并且不希望數(shù)據(jù)變化時(shí)引起組件 re-render, 而只是在回調(diào)函數(shù)中需要獲取到對(duì)應(yīng)內(nèi)容時(shí),推薦使用 Ref。如 interval id 。
以上就是React中Refs的使用場(chǎng)景及核心要點(diǎn)詳解的詳細(xì)內(nèi)容,更多關(guān)于React Refs的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React實(shí)現(xiàn)二級(jí)聯(lián)動(dòng)的方法
這篇文章主要為大家詳細(xì)介紹了React實(shí)現(xiàn)二級(jí)聯(lián)動(dòng)的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09react 不用插件實(shí)現(xiàn)數(shù)字滾動(dòng)的效果示例
這篇文章主要介紹了react 不用插件實(shí)現(xiàn)數(shù)字滾動(dòng)的效果示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04React之防止按鈕多次點(diǎn)擊事件?重復(fù)提交
這篇文章主要介紹了React之防止按鈕多次點(diǎn)擊事件?重復(fù)提交問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10React-intl 實(shí)現(xiàn)多語(yǔ)言的示例代碼
本篇文章主要介紹了React-intl 實(shí)現(xiàn)多語(yǔ)言的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11react使用useState修改對(duì)象或者數(shù)組的值無(wú)法改變視圖的問(wèn)題
這篇文章主要介紹了react使用useState修改對(duì)象或者數(shù)組的值無(wú)法改變視圖的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過(guò)程詳解
這篇文章主要介紹了使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過(guò)程詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04