概述
在Function Component項目中當我們需要操作dom的時候,第一時間想到的就是使用useRef這個Hook來綁定dom。但是這個僅僅是使用這個Hook而已,為了更好的學習React Hooks內部實現原理,知其所以然。所以本文根據源碼從useRef的基礎使用場景一步一步到內部實現來對其進行介紹。
基本使用
在React中useRef是這樣定義的:useRef保存一個可變的持久化引用,重新渲染時不會重值,更新值也不會渲染頁面
。
export function useRef<T>(initialValue: T): { current: T } {const dispatcher = resolveDispatcher();return dispatcher.useRef(initialValue);
}
由代碼能看出useRef接收任意類型的值,包含普通值、函數、dom,然后經過dispather進行派發處理,返回一個包含current屬性的對象引用,該對象和普通Js對象一致,更新不收React約束。
一般在項目中useRef常用的有兩個使用場景:
- 通過useRef保持持久化的值,且不需要重新渲染
- 通過useRef綁定dom,以便直接進行dom操作
比如在項目中常用的定時器,我們都會在組件銷毀時通過clear函數
進行定時器的清除避免內存泄露等問題,這時候就可以通過useRef來綁定timerId
import { useRef } from 'react';let timerId = useRef(null);useEffect(() => {timerId.current = setInterval(() => {console.log('setInterval');}, 1000);return () => {clearInterval(timerId.current);}
}, [])export default function Counter() {return <></>
}
當我們需要進行dom操作時,比如獲取焦點、自動滾動等,就可以通過useRef來綁定dom進行操作
import { useRef } from 'react';let inputRef = useRef(null);useEffect(() => {// 在組件掛載后聚焦輸入框inputRef.current.focus();
}, [])export default function Counter() {return <input ref={inputRef} type='text' />
}
源碼解析
由于這里的mount、update邏輯很簡單,并當useRef傳遞值/函數和傳遞dom時的處理是不一樣的,所以我們以此來分開介紹。
傳遞普通值時
當傳遞普通值時(包含任意類型值、函數),主要執行mountRef、updateRef
兩個函數。在mount掛載時創建一個包含current屬性的對象,然后在更新時返回相同的引用memoizedState保存的
,所以這里就在一起寫了。
function mountRef<T>(initialValue: T): { current: T } {// 創建hook鏈表const hook = mountWorkInProgressHook();// ref初始化const ref = { current: initialValue };hook.memoizedState = ref;// 返回refreturn ref;
}function updateRef<T>(initialValue: T): { current: T } {// 復用hookconst hook = updateWorkInProgressHook();// 返回相同引用return hook.memoizedState;
}
從源碼能看出,useRef接收一個初始化參數,可以為值/返回值的函數,然后在mountRef中創建了一個包含current的對象,在updateRef中仍然返回的該對象引用。
如果初始值是函數,因為React內部不會做判斷,直接將初始值賦予current,如何是函數,則需要手動顯式調用
由于不管在mount掛載時,還是在update更新時都是返回的對象引用,以此來保持持久化,當我們通過ref.current修改值時本質修改的是同一個引用對象,所以也不會觸發重新渲染(object.is對比一直都是true)。
傳遞DOM時
當傳遞DOM時,在mount、update階段也和傳值一樣,不會做任何處理會返回相應的對象引用,但是如果傳遞的是DOM時,在Reconciler協調器中通過React.createElement將JSX轉換為React元素后進行fiber構造
,在構造完成生產fiber樹之后會進入到commit階段,在該階段會遍歷節點對副作用和ref進行處理,其中在layout階段會判斷當前節點類型(tag)如何是dom(tag === HostComponent
)時,如果該dom有ref,則會對ref進行處理commitAttachRef函數
在commit階段,即renderer階段,針對dom的不同狀態和處理分為了三個階段: Before Mutation、Mutation、Layout。有興趣的可以查看這篇文章【React源碼 - Fiber架構之Renderer】
以下commitAttachRef代碼(省略了部分代碼):
function commitAttachRef(finishedWork: Fiber) {// 獲取節點的ref屬性const ref = finishedWork.ref;if (ref !== null) {// 獲取dom實例,fiber.stateNode就是綁定的dom,在completeWork中會創建dom然后綁定到fiber.stateNode上const instance = finishedWork.stateNode;let instanceToUse;switch (finishedWork.tag) {case HostHoistable:case HostSingleton:case HostComponent:// 獲取dom實例instanceToUse = getPublicInstance(instance);break;default:instanceToUse = instance;}//if (typeof ref === "function") {// 將dom實例回傳給傳遞的ref函數finishedWork.refCleanup = ref(instanceToUse);} else {// 普通對象賦值到currentref.current = instanceToUse;}}
}
從代碼能看出該函數主要就是獲取ref綁定的dom實例,然后根據傳入ref的不同進行處理,如果是函數則將dom實例傳遞給函數由開發者顯式調用,否則則綁定到current屬性上進行返回。
傳遞函數,顯式處理ref的demo:
import React, { useEffect, useRef } from 'react';function App() {const divRef = useRef(null);useEffect(() => {if (divRef.current) {console.log('Element mounted:', divRef.current);}return () => {console.log('Element unmounted:', divRef.current);};}, []);return <div ref={divRef}>Hello, World!</div>;
}export default App;
總結
基于以上了解,我們知道了useRef的基礎使用和場景以及背后的代碼處理,簡要總結一下就是:useRef用于持久化引用,返回普通Js引用,修改其值不會導致組件重新渲染。當傳遞普通值時,不會進行特殊處理,只是返回相同的對象引用。當綁定dom時,在mount、update階段初始化對象,然后在commit階段進行ref處理,函數顯式處理則會將dom實例作為參數回傳,普通值則會綁定到ref.current中
。