問題
事件處理函數(shù)會(huì)被重復(fù)定義
數(shù)據(jù)計(jì)算過程沒有緩存
useCallback - 緩存回調(diào)函數(shù)
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
// ...
return <button onClick={handleIncrement}>+</button>
}
每次組件狀態(tài)發(fā)生變化的時(shí)候,函數(shù)組件實(shí)際上都會(huì)重新執(zhí)行一遍铛铁。在每次執(zhí)行的時(shí)候隔显,實(shí)際上都會(huì)創(chuàng)建一個(gè)新的事件處理函數(shù) handleIncrement
。這個(gè)事件處理函數(shù)中呢避归,包含了 count
這個(gè)變量的閉包荣月,以確保每次能夠得到正確的結(jié)果。
即使 count 沒有發(fā)生變化梳毙,但是函數(shù)組件因?yàn)槠渌鼱顟B(tài)發(fā)生變化而重新渲染時(shí)哺窄,這種寫法也會(huì)每次創(chuàng)建一個(gè)新的函數(shù)。創(chuàng)建一個(gè)新的事件處理函數(shù),雖然不影響結(jié)果的正確性萌业,但其實(shí)是沒必要的坷襟。因?yàn)檫@樣做不僅增加了系統(tǒng)的開銷,更重要的是:每次創(chuàng)建新函數(shù)的方式會(huì)讓接收事件處理函數(shù)的組件生年,需要重新渲染婴程。
比如這個(gè)例子中的 button
組件,接收了 handleIncrement
抱婉,并作為一個(gè)屬性档叔。如果每次都是一個(gè)新的,那么這個(gè) React 就會(huì)認(rèn)為這個(gè)組件的 props 發(fā)生了變化蒸绩,從而必須重新渲染衙四。因此,我們需要做到的是:只有當(dāng) count 發(fā)生變化時(shí)患亿,我們才需要重新定一個(gè)回調(diào)函數(shù)传蹈。而這正是 useCallback
這個(gè) Hook 的作用。
import React, { useState, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count], // 只有當(dāng) count 發(fā)生變化時(shí)步藕,才會(huì)重新創(chuàng)建回調(diào)函數(shù)
);
// ...
return <button onClick={handleIncrement}>+</button>
}
useMemo - 緩存計(jì)算結(jié)果
如果某個(gè)數(shù)據(jù)是通過其它數(shù)據(jù)計(jì)算得到的惦界,那么只有當(dāng)用到的數(shù)據(jù),也就是依賴的數(shù)據(jù)發(fā)生變化的時(shí)候咙冗,才應(yīng)該需要重新計(jì)算沾歪。
- 避免重復(fù)計(jì)算。
- 避免子組件的重復(fù)渲染乞娄。
useCallback 的功能可以用 useMemo 來實(shí)現(xiàn)
建立了一個(gè)綁定某個(gè)結(jié)果到依賴數(shù)據(jù)的關(guān)系瞬逊。只有當(dāng)依賴變了,這個(gè)結(jié)果才需要被重新得到仪或。
// 只有當(dāng) count 發(fā)生變化時(shí),才會(huì)重新創(chuàng)建回調(diào)函數(shù) );
const handleIncrement = useCallback( () => setCount(count + 1), [count],
const myEventHandler = useMemo(() => {
// 返回一個(gè)函數(shù)作為緩存結(jié)果
return () => {
// 在這里進(jìn)行事件處理
setCount(count + 1)
}
}, [count]);
useRef - 在多次渲染之間共享數(shù)據(jù)
我們可以把 useRef 看作是在函數(shù)組件之外創(chuàng)建的一個(gè)容器空間士骤。在這個(gè)容器上范删,我們可以通過唯一的 current 屬設(shè)置一個(gè)值,從而在函數(shù)組件的多次渲染之間共享這個(gè)值拷肌。
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定義 time state 用于保存計(jì)時(shí)的累積時(shí)間
const [time, setTime] = useState(0);
// 定義 timer 這樣一個(gè)容器用于在跨組件渲染之間保存一個(gè)變量
const timer = useRef(null);
// 開始計(jì)時(shí)的事件處理函數(shù)
const handleStart = useCallback(() => {
// 使用 current 屬性設(shè)置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暫停計(jì)時(shí)的事件處理函數(shù)
const handlePause = useCallback(() => {
// 使用 clearInterval 來停止計(jì)時(shí)
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
使用 useRef 保存的數(shù)據(jù)一般是和 UI 的渲染無關(guān)的到旦,因此當(dāng) ref 的值發(fā)生變化時(shí),是不會(huì)觸發(fā)組件的重新渲染的
除了存儲(chǔ)跨渲染的數(shù)據(jù)之外巨缘,useRef 還有一個(gè)重要的功能添忘,就是保存某個(gè) DOM 節(jié)點(diǎn)的引用。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 屬性指向了真實(shí)的 input 這個(gè) DOM 節(jié)點(diǎn)若锁,從而可以調(diào)用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useContext - 定義全局狀態(tài)
解決跨層次搁骑,或者同層的組件之間進(jìn)行數(shù)據(jù)共享的問題
React
提供了Context
這樣一個(gè)機(jī)制,能夠讓所有在某個(gè)組件開始的組件樹上創(chuàng)建一個(gè)Context
。這樣這個(gè)組件樹上的所有組件仲器,就都能訪問和修改這個(gè)Context
了煤率。那么在函數(shù)組件里,我們就可以使用useContext
這樣一個(gè)Hook
來管理Context
乏冀。
定義Context
// 創(chuàng)建ThemeContext
const ThemeContext = React.createContext(themes.light);
使用Context.Provider作為根節(jié)點(diǎn)
function App() {
// 整個(gè)應(yīng)用使用 ThemeContext.Provider 作為根組件
return (
// 使用 themes.dark 作為當(dāng)前 Context, themes中保存了一些主題
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
子組件中用useContext讀取Context.Provider提供的值
// 在 Theme Button 中使用 useContext 來獲取當(dāng)前的主題
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme.background,
color: theme.foreground
}}>
I am styled by theme context!
</button>
);
}
Context
Context
相當(dāng)于提供了一個(gè)定義 React
世界中全局變量的機(jī)制蝶糯,而全局變量則意味著兩點(diǎn):
- 會(huì)讓調(diào)試變得困難,因?yàn)槟愫茈y跟蹤某個(gè)
Context
的變化究竟是如何產(chǎn)生的辆沦。 - 讓組件的復(fù)用變得困難昼捍,因?yàn)橐粋€(gè)組件如果使用了某個(gè)
Context
,它就必須確保被用到的地方一定 有這個(gè)Context
的Provider
在其父組件的路徑上肢扯。
所以在
React
的開發(fā)中端三,除了像Theme
、Language
等一目了然的需要全局設(shè)置的變量外鹃彻,我們很少會(huì)使用Context
來做太多數(shù)據(jù)的共享郊闯。
需要再三強(qiáng)調(diào)的是,Context
更多的是提供了一個(gè)強(qiáng)大的機(jī)制蛛株,讓React
應(yīng)用具備定義全局的響應(yīng)式數(shù)據(jù)的能力团赁。
很多狀態(tài)管理框架,比如 Redux
谨履,正是利用了 Context
的機(jī)制來提供一種更加可控的組件之間的狀 態(tài)管理機(jī)制欢摄。因此,理解 Context
的機(jī)制笋粟,也可以讓我們更好地去理解 Redux
這樣的框架實(shí)現(xiàn)的原理怀挠。
思考題
useState 其實(shí)也是能夠在組件的多次渲染之間共享數(shù)據(jù)的,那么在 useRef 的計(jì)時(shí)器例子中害捕,我們能否用 state 去保存 window.setInterval() 返回的 timer 呢绿淋?