深入理解React Ref 機制useRef 與 forwardRef 的協(xié)作原理
1. 歷史背景:從命令式到聲明式的演變
1.1 Class 組件時代:命令式開發(fā)
在 React 早期,我們使用 Class 組件,開發(fā)模式本質上是命令式的。
什么是命令式?
命令式編程關注"怎么做"——你需要一步步告訴計算機執(zhí)行什么操作
?? 代碼示例(Class 組件 - 命令式):
// 點擊按鈕,輸入框背景變黃、聚焦、并選中所有文字
class OldForm extends React.Component {
constructor(props) {
super(props);
// ═══════════════════════════════════════════════════════════════════
// 【命令式特點 ①】:必須手動聲明變量來“抓住”DOM 元素
// 就像手里必須拿著一根繩子拴著這個元素,否則找不到它
// ═══════════════════════════════════════════════════════════════════
this.inputEl = null;
}
// ═══════════════════════════════════════════════════════════════════
// 【命令式特點 ②】:開發(fā)者必須像“指揮官”一樣,一步步下達指令
// 1. 找到元素 -> 2. 改顏色 -> 3. 聚焦 -> 4. 選中文本
// 順序不能亂,漏一步都不行
// ═══════════════════════════════════════════════════════════════════
handleHighlight = () => {
// ?? 第一步:手動修改樣式屬性
this.inputEl.style.backgroundColor = 'yellow';
// ?? 第二步:手動調用聚焦方法
this.inputEl.focus();
// ?? 第三步:手動調用選中方法
this.inputEl.select();
// ?? 痛點:如果 inputEl 為 null,這里直接報錯崩潰
// 開發(fā)者需要自己防御性編程 (if (!this.inputEl) return;)
};
render() {
return (
<div>
<input
// ═══════════════════════════════════════════════════════════════════
// 【命令式特點 ③】:使用回調函數(shù)“捕獲”DOM
// 每次渲染都要執(zhí)行這個函數(shù),把 DOM 元素存到 this.inputEl 里
// ═══════════════════════════════════════════════════════════════════
ref={el => this.inputEl = el}
defaultValue="請高亮我"
/>
{/* ?? 按鈕綁定的不是狀態(tài)變化,而是一個具體的“動作” */}
<button onClick={this.handleHighlight}>
高亮并選中 (命令式)
</button>
</div>
);
}
}
// ═══════════════════════════════════════════════════════════════════
// 【核心邏輯】:UI = 一系列指令的集合 (Do A, then Do B, then Do C)
// ═══════════════════════════════════════════════════════════════════命令式的特點:
特點 | 說明 |
手動管理 DOM | 開發(fā)者需要自己找到元素、修改屬性、綁定事件 |
狀態(tài)同步復雜 | 數(shù)據變了要更新 DOM,DOM 變了要更新數(shù)據 |
生命周期繁瑣 | 需要在 componentDidMount、componentDidUpdate 中處理副作用 |
難以追蹤 | 隨著代碼量增加,誰在什么時候修改了 DOM 很難追蹤 |
1.2 函數(shù)組件時代:聲明式開發(fā)
隨著 React Hooks 的推出,函數(shù)組件成為主流,開發(fā)模式轉向聲明式。
什么是聲明式?
聲明式編程關注"是什么"——你描述界面應該長什么樣,React 負責幫你實現(xiàn)。
?? 代碼示例(函數(shù)組件 - 聲明式):輸入框自動聚焦
//點擊按鈕,輸入框背景變黃、聚焦、并選中所有文字
import { useRef, useEffect, useState } from 'react';
function ModernForm() {
const inputRef = useRef(null);
// ═══════════════════════════════════════════════════════════════════
// 【聲明式特點 ①】:只關心“狀態(tài)是什么”,不關心“怎么變”
// 我們定義一個狀態(tài) isActive,代表“是否處于高亮模式”
// ═══════════════════════════════════════════════════════════════════
const [isActive, setIsActive] = useState(false);
// ═══════════════════════════════════════════════════════════════════
// 【聲明式特點 ②】:副作用是“響應”狀態(tài)變化的
// 邏輯:當 isActive 變?yōu)?true 時 -> 自動執(zhí)行聚焦和選中
// 開發(fā)者不需要告訴 React“什么時候”做,只需定義“什么條件下”做
// ═══════════════════════════════════════════════════════════════════
useEffect(() => {
if (isActive) {
// ? 只有當狀態(tài)改變時,這些命令式操作才會被執(zhí)行
inputRef.current?.focus();
inputRef.current?.select();
// 可選:執(zhí)行完后重置狀態(tài),或者保持高亮
// 這里為了演示持續(xù)高亮,暫不重置
}
}, [isActive]); // ?? 關鍵:依賴項驅動,狀態(tài)變 -> 效應動
const handleClick = () => {
// ═══════════════════════════════════════════════════════════════════
// 【聲明式特點 ③】:只更新數(shù)據,不操作 DOM
// 點擊只是改變了“狀態(tài)”,具體的變色、聚焦由 React 和 useEffect 協(xié)調完成
// ═══════════════════════════════════════════════════════════════════
setIsActive(true);
};
return (
<div>
<input
ref={inputRef}
defaultValue="請高亮我"
// ═══════════════════════════════════════════════════════════════════
// 【聲明式特點 ④】:樣式是狀態(tài)的“映射”
// 背景色 = isActive ? '黃色' : '默認'
// 只要狀態(tài)對了,樣式自然就對,不需要手動 style.xxx
// ═══════════════════════════════════════════════════════════════════
style={{
backgroundColor: isActive ? 'yellow' : 'transparent'
}}
/>
<button onClick={handleClick}>
高亮并選中 (聲明式)
</button>
</div>
);
}
// ═══════════════════════════════════════════════════════════════════
// 【核心邏輯】:UI = f(State) (界面是狀態(tài)的函數(shù))
// State 變了 -> UI 自動重新計算并渲染
// ═══════════════════════════════════════════════════════════════════聲明式的優(yōu)勢:
優(yōu)勢 | 說明 |
代碼簡潔 | 無需手動操作 DOM,React 自動處理 |
可預測性強 | UI 完全由 State 決定,易于調試 |
負擔低 | 只需關注數(shù)據變化,不用想 DOM 更新細節(jié) |
性能優(yōu)化 | React 自動批量更新,減少不必要的重繪 |
1.3 為什么要從命令式轉變?yōu)槁暶魇剑?/h3>
對比維度 | 命令式 (Class) | 聲明式 (Hooks) |
代碼量 | 多(需寫生命周期) | 少(只需寫邏輯) |
狀態(tài)管理 | 分散(this.state + DOM) | 集中(useState) |
復用性 | 低(HOC/Render Props 復雜) | 高(Custom Hooks) |
學習曲線 | 陡峭(this 綁定、生命周期) | 平緩(函數(shù)思維) |
轉變的必要性:
- 可維護性:大型項目中,命令式代碼難以維護
- 協(xié)作效率:聲明式代碼更易讀,團隊協(xié)作更順暢
- 生態(tài)發(fā)展:Hooks 生態(tài)更豐富,社區(qū)支持更好
2. 聲明式的邊界:什么是瀏覽器行為
2.1 聲明式的局限性
雖然聲明式很優(yōu)雅,但它無法處理所有場景。
?? 思考題:
// ? 這樣寫沒用!
function Input() {
const [isFocused, setIsFocused] = useState(false);
// 設置 isFocused = true 并不會讓輸入框真正聚焦
// 因為聚焦是瀏覽器的行為,不是 React 的狀態(tài)
return <input autoFocus={isFocused} />;
}2.2 什么是瀏覽器行為?
瀏覽器行為指的是那些瞬時的、副作用的、無法通過狀態(tài)驅動的操作:
瀏覽器行為 | 說明 | 為什么聲明式無法處理 |
| 聚焦/失焦 | 這是瞬時動作,不是狀態(tài) |
| 滾動到指定位置 | 這是視口行為 |
| 選中文本 | 這是編輯器行為 |
| 播放/暫停媒體 | 這是媒體控制行為 |
Canvas 繪圖 | 逐幀繪制 | 這是即時渲染行為 |
第三方庫集成 | jQuery/D3 等 | 它們直接操作 DOM |
核心區(qū)別:
聲明式:UI = f(State) → 描述"界面應該長什么樣" 命令式:DOM.action() → 執(zhí)行"現(xiàn)在立刻做什么"
3. useRef:聲明式世界里的命令式逃生艙
3.1 useRef 的基本用法
為了解決聲明式無法處理瀏覽器行為的問題,React 提供了 useRef。
?? 代碼示例(基礎用法):
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null); // ① 創(chuàng)建 ref
useEffect(() => {
// ② 在副作用中操作 DOM
inputRef.current.focus();
}, []);
return (
// ③ 將 ref 綁定到 DOM 元素
<input ref={inputRef} placeholder="我會自動聚焦" />
);
}3.2 useRef 的核心特性
特性 | 說明 |
| 可以調用所有原生 DOM API |
修改 ref 不觸發(fā)重渲染 | 與 useState 的最大區(qū)別 |
生命周期持久 | 組件重新渲染時,ref 保持引用不變 |
可存儲任意值 | 不僅是 DOM,也可以是定時器、前一次的值等 |
?? 代碼示例(對比 useState 和 useRef):
function Compare() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const updateState = () => {
setCount(count + 1); // ? 觸發(fā)重渲染
};
const updateRef = () => {
countRef.current = countRef.current + 1; // ? 不觸發(fā)重渲染
console.log(countRef.current); // 但值確實變了
};
return (
<div>
<p>State: {count}</p> {/* 會顯示最新值 */}
<p>Ref: {countRef.current}</p> {/* 不會自動更新顯示 */}
</div>
);
}3.3 useRef 解決了什么痛點?
痛點 | 解決方案 |
無法操作瞬時瀏覽器行為 | 通過 |
需要在渲染間保持可變值 | ref 的值在渲染間持久保存 |
需要訪問前一次的值 | 用 ref 存儲上一輪的值 |
需要存儲定時器/訂閱 | 避免內存泄漏 |
?? 代碼示例(存儲定時器):
function TimerComponent() {
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
console.log('tick');
}, 1000);
// 清理定時器
return () => clearInterval(timerRef.current);
}, []);
return <div>計時中...</div>;
}3.4 useRef 的新痛點
雖然 useRef 很好用,但它有一個致命限制:
?? 代碼示例(問題演示):
// 子組件:自定義函數(shù)組件
function MyInput(props) {
// ?? 這里收不到 ref!
return <input {...props} />;
}
// 父組件
function Parent() {
const inputRef = useRef(null);
useEffect(() => {
console.log(inputRef.current); // ? 輸出:null
}, []);
return <MyInput ref={inputRef} />; // ref 被"吃掉"了
}為什么會被"吃掉"?
- React 設計時,
ref和key一樣,是特殊屬性 - 它們不會被放入
props對象中傳遞給函數(shù)組件 - 這是為了保持函數(shù)組件的純粹性(函數(shù)組件沒有實例)
4. 封裝的代價:ref 在組件傳遞中的困境
4.1 真實開發(fā)場景
在實際項目中,我們很少直接在父組件中寫原生 <input>,而是會:
- 封裝成通用組件
<MyInput /> - 使用 UI 庫組件(如 AntD 的
<Input />)
?? 代碼示例(封裝場景):
// --- 子組件:封裝的輸入框 ---
function MyInput(props) {
return (
<div className="input-wrapper">
<label>用戶名:</label>
<input {...props} />
</div>
);
}
// --- 父組件:試圖控制子組件 ---
function Form() {
const inputRef = useRef(null);
const handleSubmit = () => {
// ? 失?。篿nputRef.current 是 null
inputRef.current?.focus();
};
return (
<form onSubmit={handleSubmit}>
<MyInput ref={inputRef} name="username" />
<button type="submit">提交</button>
</form>
);
}4.2 問題根源分析
┌─────────────────────────────────────────────────────────┐
│ 父組件 │
│ const inputRef = useRef(null) │
│ inputRef.current → ??? (null) │
│ ↓ │
│ <MyInput ref={inputRef} /> │
│ ↓ │
│ ┌───────────────────┐ │
│ │ 子組件 MyInput │ │
│ │ │ │
│ │ function(props) │ │
│ │ ?? ref 不進入 props │ │
│ │ │ │
│ │ <input {...props}│ │
│ │ ref 丟失! │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘4.3 錯誤嘗試:手動傳遞 ref
有些人會嘗試這樣解決:
// ? 錯誤做法:把 ref 當作普通 prop 傳遞
function MyInput({ inputRef, ...props }) {
return <input {...props} ref={inputRef} />;
}
function Parent() {
const ref = useRef(null);
return <MyInput inputRef={ref} />; // 不推薦!
}為什么不推薦?
- 命名不統(tǒng)一,容易造成混淆
- 不符合 React 的設計規(guī)范
- 無法與 React 的 ref 系統(tǒng)正確集成
5. forwardRef:打通組件邊界的橋梁
5.1 forwardRef 的引入
為了解決 ref 無法傳遞給函數(shù)組件的問題,React 提供了 forwardRef。
核心作用:
5.2 基本用法
?? 代碼示例(修復封裝問題):
import { forwardRef } from 'react';
// --- 子組件:使用 forwardRef ---
const MyInput = forwardRef((props, ref) => { // ① 接收第二個參數(shù) ref
return (
<div className="input-wrapper">
<label>用戶名:</label>
{/* ② 將 ref 綁定到內部 DOM */}
<input {...props} ref={ref} />
</div>
);
});
// --- 父組件:無需修改 ---
function Form() {
const inputRef = useRef(null);
const handleSubmit = () => {
// ? 成功:inputRef.current 指向內部的 <input>
inputRef.current?.focus();
};
return (
<form onSubmit={handleSubmit}>
<MyInput ref={inputRef} name="username" />
<button type="submit">提交</button>
</form>
);
}5.3 協(xié)作原理圖解
┌─────────────────────────────────────────────────────────┐
│ 父組件 │
│ const inputRef = useRef(null) │
│ inputRef.current → <input> DOM 節(jié)點 ? │
│ ↓ │
│ <MyInput ref={inputRef} /> │
│ ↓ │
│ ┌───────────────────┐ │
│ │ forwardRef 包裝 │ │
│ │ (轉發(fā) ref) │ │
│ └───────────────────┘ │
│ ↓ │
│ ┌───────────────────┐ │
│ │ 子組件 MyInput │ │
│ │ │ │
│ │ (props, ref) │ ← ① ref 作為第二參數(shù) │
│ │ │ │
│ │ <input ref={ref} │ ← ② 綁定到內部 DOM │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘5.4 完整協(xié)作流程
表格
步驟 | 代碼 | 說明 |
1. 父組件創(chuàng)建 ref |
| 創(chuàng)建引用容器 |
2. 父組件傳遞 ref |
| 將 ref 傳給子組件 |
3. 子組件接收 ref |
| ref 作為第二參數(shù) |
4. 子組件綁定 DOM |
| 將 ref 綁定到內部元素 |
5. 父組件操作 DOM |
| 成功調用 DOM API |
6. useImperativeHandle:自定義暴露接口的進階方案
6.1 為什么需要 useImperativeHandle?
使用 forwardRef 后,父組件可以拿到子組件內部的 DOM 節(jié)點。但這帶來了一個新問題:
?? 代碼示例(暴露過多的問題):
// 子組件
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
// 父組件
function Parent() {
const inputRef = useRef(null);
const handleClick = () => {
// ?? 問題:父組件可以訪問 input 的所有屬性和方法
inputRef.current.focus(); // 這是預期的
inputRef.current.value = 'hacked'; // 這也可以!
inputRef.current.style.color = 'red'; // 這也可以!
inputRef.current.form.submit(); // 甚至可以訪問表單!
};
return <MyInput ref={inputRef} />;
}這違背了什么原則?
- 封裝原則:子組件的內部實現(xiàn)不應該完全暴露給父組件
- 最小權限原則:父組件應該只擁有它需要的能力
- 接口穩(wěn)定性:如果子組件內部 DOM 結構變化,父組件代碼可能崩潰
6.2 useImperativeHandle 的作用
useImperativeHandle 允許子組件自定義暴露給父組件的 ref 值。
核心功能:
不是直接暴露 DOM 節(jié)點,而是暴露一個自定義對象,只包含你想要暴露的方法。
6.3 基本用法
?? 代碼示例(使用 useImperativeHandle):
import { forwardRef, useImperativeHandle, useRef } from 'react';
// --- 子組件:自定義暴露接口 ---
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 自定義暴露給父組件的方法
useImperativeHandle(ref, () => ({
// 只暴露 focus 方法
focus: () => {
inputRef.current?.focus();
},
// 只暴露 select 方法
select: () => {
inputRef.current?.select();
},
// 只暴露 getValue 方法
getValue: () => {
return inputRef.current?.value || '';
},
// 只暴露 setValue 方法
setValue: (value) => {
if (inputRef.current) {
inputRef.current.value = value;
}
}
}));
return <input {...props} ref={inputRef} />;
});
// --- 父組件:只能調用暴露的方法 ---
function Parent() {
const inputRef = useRef(null);
const handleClick = () => {
// ? 可以調用暴露的方法
inputRef.current?.focus();
inputRef.current?.select();
const value = inputRef.current?.getValue();
// ? 以下操作會報錯(因為不是暴露的方法)
// inputRef.current.value = 'hacked'; // undefined
// inputRef.current.style.color = 'red'; // undefined
};
return <MyInput ref={inputRef} />;
}6.4 協(xié)作原理圖解
┌─────────────────────────────────────────────────────────┐
│ 父組件 │
│ const inputRef = useRef(null) │
│ inputRef.current → { focus, select, getValue } ? │
│ ↓ │
│ <MyInput ref={inputRef} /> │
│ ↓ │
│ ┌───────────────────┐ │
│ │ forwardRef 包裝 │ │
│ └───────────────────┘ │
│ ↓ │
│ ┌───────────────────┐ │
│ │ 子組件 MyInput │ │
│ │ │ │
│ │ useImperative │ │
│ │ Handle(ref, ()=> │ │
│ │ { focus, ... }) │ ← 自定義返回對象 │
│ │ │ │
│ │ const inputRef │ │
│ │ = useRef(null) │ ← 內部真實 DOM 引用 │
│ │ │ │
│ │ <input ref={ │ │
│ │ inputRef} /> │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘6.5 對比:直接暴露 DOM vs 自定義暴露
對比維度 | 直接暴露 DOM | 自定義暴露 (useImperativeHandle) |
封裝性 | ? 差,內部結構完全暴露 | ? 好,只暴露必要接口 |
安全性 | ? 低,父組件可任意修改 | ? 高,父組件只能調用指定方法 |
靈活性 | ? 低,受限于 DOM API | ? 高,可以組合多個操作 |
可維護性 | ? 低,DOM 結構變化會影響父組件 | ? 高,內部實現(xiàn)變化不影響父組件 |
TypeScript 支持 | ? 自動推斷 DOM 類型 | ? 可自定義接口類型 |
6.6 進階場景:組合多個內部 ref
有時子組件內部有多個 DOM 節(jié)點,我們可以用 useImperativeHandle 組合它們:
?? 代碼示例(組合多個 ref):
const ComplexInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const labelRef = useRef(null);
const errorRef = useRef(null);
useImperativeHandle(ref, () => ({
// 組合多個內部 ref 的操作
focus: () => {
inputRef.current?.focus();
labelRef.current?.classList.add('focused');
},
blur: () => {
inputRef.current?.blur();
labelRef.current?.classList.remove('focused');
},
showError: (message) => {
if (errorRef.current) {
errorRef.current.textContent = message;
errorRef.current.style.display = 'block';
}
},
hideError: () => {
if (errorRef.current) {
errorRef.current.style.display = 'none';
}
},
validate: () => {
const value = inputRef.current?.value || '';
if (value.length < 3) {
this.showError('至少需要 3 個字符');
return false;
}
this.hideError();
return true;
}
}));
return (
<div className="complex-input">
<label ref={labelRef}>{props.label}</label>
<input {...props} ref={inputRef} />
<div ref={errorRef} className="error" style={{ display: 'none' }} />
</div>
);
});6.7 進階場景:依賴項優(yōu)化
useImperativeHandle 接受第三個參數(shù) deps,用于優(yōu)化性能:
?? 代碼示例(添加依賴項):
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [isValid, setIsValid] = useState(true);
useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current?.focus();
},
// 方法內部可以使用組件的狀態(tài)
validate: () => {
const value = inputRef.current?.value || '';
const result = value.length >= 3;
setIsValid(result); // 可以觸發(fā)狀態(tài)更新
return result;
},
// 可以訪問 props
getProps: () => {
return {
name: props.name,
placeholder: props.placeholder
};
}
}),
[props.name, props.placeholder] // 依賴項,變化時重新創(chuàng)建方法
);
return <input {...props} ref={inputRef} />;
});依賴項的作用:
場景 | 是否需要 deps | 說明 |
方法內部使用 props | ? 需要 | props 變化時需要更新方法 |
方法內部使用 state | ? 需要 | state 變化時需要更新方法 |
方法只操作 DOM | ? 可選 | DOM 引用穩(wěn)定,可不加 |
方法調用其他函數(shù) | ? 需要 | 被調用的函數(shù)變化時需要更新 |
7. 最佳實踐與注意事項
7.1 何時使用 useRef + forwardRef + useImperativeHandle?
場景 | 推薦方案 | 說明 |
自動聚焦輸入框 | ? useRef + forwardRef | 簡單場景,直接暴露 DOM |
表單組件庫 | ? + useImperativeHandle | 需要封裝,限制外部訪問 |
集成第三方庫 | ? useRef | 直接操作 DOM |
滾動到指定位置 | ? useRef + forwardRef | 需要跨組件控制 |
表單數(shù)據提交 | ? 優(yōu)先使用受控組件 | 能用 State 解決的不用 ref |
樣式變化 | ? 優(yōu)先使用 className/State | 聲明式更合適 |
條件渲染 | ? 優(yōu)先使用 State | ref 不適合控制渲染邏輯 |
核心原則:
┌────────────────────────────────────────────────────────────┐ │ Ref 使用決策樹 │ ├────────────────────────────────────────────────────────────┤ │ │ │ 需要操作 DOM 嗎? │ │ ↓ 是 │ │ 是原生 DOM 元素嗎? │ │ ↓ 是 → 直接用 useRef │ │ ↓ 否 │ │ 是自定義函數(shù)組件嗎? │ │ ↓ 是 → 用 forwardRef │ │ ↓ │ │ 需要限制父組件訪問權限嗎? │ │ ↓ 是 → 加上 useImperativeHandle │ │ ↓ 否 → 直接轉發(fā) ref │ │ │ └────────────────────────────────────────────────────────────┘
7.2 注意事項
?? 注意 1:forwardRef 會創(chuàng)建新組件
const MyComponent = forwardRef(...); // 需要手動設置 displayName,方便調試 MyComponent.displayName = 'MyComponent';
?? 注意 2:ref 不能用在條件渲染中
?? 注意 3:避免過度使用
// ? 能用 State 解決的,不要用 ref
function BadExample() {
const countRef = useRef(0); // 應該用 useState
const increment = () => {
countRef.current++; // 不會觸發(fā)重渲染
};
}
// ? 正確做法
function GoodExample() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // 會觸發(fā)重渲染
};
}?? 注意 4:useImperativeHandle 的依賴項
// ? 忘記加依賴項,方法內部使用過時的 props/state
useImperativeHandle(ref, () => ({
getValue: () => props.defaultValue // 可能不是最新值
}));
// ? 加上依賴項
useImperativeHandle(ref, () => ({
getValue: () => props.defaultValue
}), [props.defaultValue]);?? 注意 5:避免循環(huán)引用
// ? 不要在 useImperativeHandle 中調用父組件傳入的函數(shù)
// 這可能導致循環(huán)依賴
useImperativeHandle(ref, () => ({
submit: () => {
props.onSubmit(); // 如果 onSubmit 又調用了 ref 的方法,會循環(huán)
}
}));7.3 性能考量
優(yōu)化點 | 說明 | 建議 |
依賴項精簡 | 只添加真正需要的依賴 | 避免不必要的重新創(chuàng)建 |
方法緩存 | 復雜方法可以用 useCallback 包裹 | 減少內存分配 |
避免大對象 | 不要暴露整個 DOM 對象 | 只暴露必要方法 |
懶加載 ref | 不常用的 ref 可以延遲創(chuàng)建 | 減少初始渲染負擔 |
8. 總結
8.1 知識回顧
┌────────────────────────────────────────────────────────────┐ │ React Ref 機制演進 │ ├────────────────────────────────────────────────────────────┤ │ │ │ Class 組件 (命令式) │ │ ↓ 轉變原因:可維護性、可預測性、性能 │ │ 函數(shù)組件 (聲明式) │ │ ↓ 局限性:無法處理瀏覽器行為 │ │ useRef (命令式逃生艙) │ │ ↓ 困境:ref 無法傳遞給函數(shù)組件 │ │ forwardRef (組件邊界橋梁) │ │ ↓ 問題:暴露過多,破壞封裝 │ │ useImperativeHandle (自定義接口) │ │ │ └────────────────────────────────────────────────────────────┘
8.2 核心要點
概念 | 核心作用 | 使用場景 | 關鍵代碼 |
聲明式 | 描述 UI 應該長什么樣 | 90% 的常規(guī)渲染場景 |
|
命令式 | 執(zhí)行瞬時操作 | 聚焦、滾動、第三方庫 |
|
useRef | 獲取 DOM 引用 | 需要操作原生 DOM 時 |
|
forwardRef | 轉發(fā) ref 給子組件 | 封裝組件需要被外部控制時 |
|
useImperativeHandle | 自定義暴露方法 | 需要限制外部訪問權限時 |
|
8.3 三者協(xié)作關系
┌────────────────────────────────────────────────────────────┐ │ 三者協(xié)作關系圖 │ ├────────────────────────────────────────────────────────────┤ │ │ │ 父組件 子組件 │ │ ┌──────────┐ ┌──────────────┐ │ │ │ useRef │ ────────→ │ forwardRef │ │ │ │ (創(chuàng)建) │ ref │ (接收并轉發(fā)) │ │ │ └──────────┘ └──────┬───────┘ │ │ │ │ │ ↓ │ │ ┌──────────────┐ │ │ │useImperative │ │ │ │Handle │ │ │ │(自定義暴露) │ │ │ └──────┬───────┘ │ │ │ │ │ ↓ │ │ ┌──────────────┐ │ │ │ 內部 useRef │ │ │ │ (真實 DOM) │ │ │ └──────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘
8.4 一句話總結
useRef 是聲明式世界里的命令式逃生艙,forwardRef 是打通組件邊界的橋梁,useImperativeHandle 是保護封裝性的守門人。三者協(xié)作讓我們既能享受聲明式的優(yōu)雅,又能保留對瀏覽器行為的終極控制權,同時不破壞組件的封裝原則。
到此這篇關于深入理解React Ref 機制useRef 與 forwardRef 的協(xié)作原理的文章就介紹到這了,更多相關React useRef 與 forwardRef 協(xié)作原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
VSCode 配置React Native開發(fā)環(huán)境的方法
本篇文章主要介紹了VSCode 配置React Native開發(fā)環(huán)境的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
forwardRef?中React父組件控制子組件的實現(xiàn)代碼
forwardRef 用于拿到父組件傳入的 ref 屬性,這樣在父組件便能通過 ref 控制子組件,這篇文章主要介紹了forwardRef?-?React父組件控制子組件的實現(xiàn)代碼,需要的朋友可以參考下2024-01-01

