一篇文章帶你理解 React 中最“臭名昭著”的 useMemo 和 useCallback
大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
原文鏈接: Understanding useMemo and useCallback[1]翻譯原文: https://juejin.cn/post/7165338403465068552譯者: oil歐喲
前言
作為一個 React 開發(fā)者,如果你一直覺得 useMemo 和 useCallback 這兩個 Hook 比較難以理解,那么別害怕,事實上很多人都如此。我和其他公司很多的 React 開發(fā)者交流過,大多數(shù)對這兩個 Hook 都是一知半解的狀態(tài)。
這篇文章就為你答疑解惑,為大家介紹這兩個 Hook 的具體作用,它們的實現(xiàn)原理以及在實際開發(fā)中如何應(yīng)用。
這篇文章更適合初/中級 React 開發(fā)者用于加深對 React 的理解,如果你才剛剛開始學(xué)習(xí) React ,那么你也可以先將這篇文章收藏起來,在你對 React 有了一定使用經(jīng)驗后再回來學(xué)習(xí)。
基礎(chǔ)概念
我們先從 useMemo 開始介紹,useMemo 的基本概念就是:它能幫助我們 “記錄” 每次渲染之間的計算值。這句話可能有些抽象,想要理解它需要你對 React 復(fù)雜的工作原理有一定的心智模型。所以我們先講講 React 的基本工作原理。
React 所做的主要事情是讓我們的 UI 與我們的 狀態(tài) 保持同步,而要實現(xiàn)它們的同步,就需要執(zhí)行一個叫做 “re-render” (重新渲染) 的操作。
每一次 重新渲染 都是一次快照,它基于當前應(yīng)用程序的狀態(tài)告訴了應(yīng)用程序的 UI 在某一特定時刻應(yīng)該是什什么樣的。我們可以把它想象成一疊照片,每張照片都記錄了在每個狀態(tài)變量的特定值下事物的樣子。
舉個例子,我們先定義一個狀態(tài) a ,它的初始值是 hello,我們先把它渲染到頁面上,這時候我們的 UI 上就會有一行 hello
const [a, setA] = useState("hello")
return (<span>{a}</span>)
如果我們將 a 設(shè)置為 world,
setA("world")
此時頁面上還是 ”hello“,為了保持狀態(tài)和 UI 同步,就需要觸發(fā)一次 重新渲染 ,這樣 UI 上也變?yōu)榱?“hello”,當然重新渲染不需要我們自己執(zhí)行 ,你在使用 setA 時 React 就會幫我們處理。
每一次 重新渲染 都會根據(jù)當前的狀態(tài)產(chǎn)生一個 DOM 應(yīng)該是什么樣子的心理圖景。在上面的例子中,我們的狀態(tài)被描繪成 HTML,但本質(zhì)上它是一堆 JS 對象。如果了解過的話就知道它也被稱為 虛擬DOM。
我們并不需要告訴 React 有哪些 DOM 節(jié)點需要改變。相反,我們告訴 React 的是基于當前狀態(tài)渲染的 UI 應(yīng)該是什么樣的。通過重新渲染,React 創(chuàng)建了一個新的快照,它可以通過比較快照找出需要改變的地方,就像玩一個 "找不同 "的游戲。
React 在你開箱使用時就進行了大量的優(yōu)化,所以一般來說,重新渲染并不是啥大問題。但是,在某些情況下,這些快照確實需要一段時間來創(chuàng)建。這可能會導(dǎo)致性能問題,比如當用戶執(zhí)行某些操作后,UI 卻不能夠快速的同步修改。
所以從本質(zhì)上,useMemo 和 useCallback 都是用來幫助我們優(yōu)化 重新渲染 的工具 Hook。它們通過以下兩種方式實現(xiàn)優(yōu)化的效果。
減少在一次渲染中需要完成的工作量。 減少一個組件需要重新渲染的次數(shù)。
下面我們通過一些實際場景介紹一下這兩個 API。
1. 需要進行大量計算的場景
假設(shè)我們寫一個工具來幫助用戶找到 0 和一個用戶傳入的數(shù)字參數(shù) selectedNum 之間的所有質(zhì)數(shù)
質(zhì)數(shù)就是一個只能被 1 和它自己整除的數(shù)字,比如17。
下面是實現(xiàn)的代碼:
import React from 'react';
function App() {
const [selectedNum, setSelectedNum] = React.useState(100);
// We calculate all of the prime numbers between 0 and the
// user's chosen number, `selectedNum`:
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}
return (
<>
<form>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
// 為了防止電腦燒起來,我們限制一下傳入的值最大為 100k
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
There are {allPrimes.length} prime(s) between 1 and {selectedNum}:
{' '}
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}
// isPrime 用于計算傳入的參數(shù)是否為質(zhì)數(shù)
function isPrime(n){
const max = Math.ceil(Math.sqrt(n));
if (n === 2) {
return true;
}
for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}
return true;
}
export default App;
你不需要看懂上面的每一行代碼,這里分析一下以上代碼的重點:
我們維護了一個狀態(tài) selectedNum我們使用一個 for循環(huán)手動計算 0 和selectedNum之間的所有質(zhì)數(shù)我們渲染了一個輸入框,用戶通過輸入改變 selectedNum的值我們在頁面中向用戶展示了所有計算出來的質(zhì)數(shù)。
以上這段代碼執(zhí)行時需要進行大量的計算。如果用戶選擇了一個值很大的 selectedNum,我們將需要遍歷數(shù)以萬計的數(shù)字去判斷每一個是否為質(zhì)數(shù)。而且即使有比我上面使用的算法更有效的素數(shù)判斷算法,但肯定也是需要進行大量計算的。
在實際開發(fā)中我們很有可能遇到類似的場景。但是有時候我們并不需要重新計算,但仍然執(zhí)行了計算操作,就有會遇到一些性能問題。比如下面這種情況:
import React from 'react';
import format from 'date-fns/format';
function App() {
const [selectedNum, setSelectedNum] = React.useState(100);
// `time` 是一個狀態(tài)變量,每秒鐘變化一次,所以它總是與當前時間同步
const time = useTime();
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}
return (
<>
<p className="clock">
{format(time, 'hh:mm:ss a')}
</p>
<form>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
// 為了防止電腦燒起來,我們限制一下傳入的值最大為 100k
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
There are {allPrimes.length} prime(s) between 1 and {selectedNum}:
{' '}
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}
function useTime() {
const [time, setTime] = React.useState(new Date());
React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
window.clearInterval(intervalId);
}
}, []);
return time;
}
// isPrime 用于計算傳入的參數(shù)是否為質(zhì)數(shù)
function isPrime(n){
const max = Math.ceil(Math.sqrt(n));
if (n === 2) {
return true;
}
for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}
return true;
}
export default App;
現(xiàn)在代碼里定義了兩個狀態(tài):selectedNum 和 time。time 每秒鐘改變一次,并且在頁面的右上角渲染出來。
這時我們會發(fā)現(xiàn)一個問題:即便我們沒有改變 selectedNum ,但是由于 time 的改變會引起重新渲染,而重新渲染又會導(dǎo)致質(zhì)數(shù)的大量計算,這樣就浪費了很多性能。
Javascript 運行時是單線程的,如果我們反復(fù)執(zhí)行這段代碼,就會一直有一個計算任務(wù)占用著線程。這會導(dǎo)致我們其他任務(wù)沒法快速執(zhí)行,整個應(yīng)用會讓人感覺很遲鈍,尤其是在低性能的設(shè)備上感知更加明顯。
那么我們該如何 繞過 這個計算的事件呢,如果我們已經(jīng)有了某個數(shù)字的質(zhì)數(shù)列表,為什么不重復(fù)使用這個值,而是每次都從頭計算呢?
這就是 useMemo 能夠幫助我們做到的事情,如下例所示:
const allPrimes = React.useMemo(() => {
const result = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
result.push(counter);
}
}
return result;
}, [selectedNum]);
useMemo 接受兩個參數(shù):
需要執(zhí)行的一些計算處理工作,包裹在一個函數(shù)中 一個依賴數(shù)組
在組件掛載的過程中,當這個組件第一次被渲染時,React 都會調(diào)用這個函數(shù)來執(zhí)行這段計算邏輯,計算所有的質(zhì)數(shù)。無論我們從這個函數(shù)中返回什么值,都會分配給 allPrimes 變量。
然而,對于每一個后續(xù)的渲染,React 都要從以下兩種情況中做出選擇:
再次調(diào)用 useMemo中的計算函數(shù),重新計算數(shù)值重復(fù)使用上一次已經(jīng)計算出來的數(shù)據(jù)
為了做出一個正確的選擇,React 會判斷你傳入的依賴數(shù)組,這個數(shù)組中的每個變量是否在兩次渲染間 值是否改變了 ,如果發(fā)生了改變,就重新執(zhí)行計算的邏輯去獲取一個新的值,否則不重新計算,直接返回上一次計算的值。
useMemo 本質(zhì)上就像一個小的緩存,而依賴數(shù)組就是緩存的失效策略。
在上面的例子中,其實本質(zhì)上是在說 “只有當 selectedNum 的值變化時才重新計算質(zhì)數(shù)列表“。 當組件因為其他情況重新渲染,例如狀態(tài) time 的值改變了,useMemo 就會忽略這個計算函數(shù),直接返回之前緩存的值。
這種緩存的過程通常被稱為 memoization,這就是為什么這個鉤子被稱為 “useMemo”。
另一種解決方法
useMemo 鉤子確實可以幫助我們避免這里不必要的計算……,但它真的是這里最好的解決方案嗎?
通常情況下,我們都會通過一些重構(gòu)來避免掉需要使用 useMemo 進行優(yōu)化的場景。如下例:
//App.js
import React from 'react';
import { getHours } from 'date-fns';
import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';
// 將 PrimeCalculator 轉(zhuǎn)換為純組件
const PurePrimeCalculator = React.memo(PrimeCalculator);
function App() {
const time = useTime();
// 基于當前時間動態(tài)計算一個背景顏色
const backgroundColor = getBackgroundColorFromTime(time);
return (
<div style={{ backgroundColor }}>
<Clock time={time} />
<PurePrimeCalculator />
</div>
);
}
const getBackgroundColorFromTime = (time) => {
const hours = getHours(time);
if (hours < 12) {
// A light yellow for mornings
return 'hsl(50deg 100% 90%)';
} else if (hours < 18) {
// Dull blue in the afternoon
return 'hsl(220deg 60% 92%)'
} else {
// Deeper blue at night
return 'hsl(220deg 100% 80%)';
}
}
function useTime() {
const [time, setTime] = React.useState(new Date());
React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
window.clearInterval(intervalId);
}
}, []);
return time;
}
export default App;
// PrimeCalculator.js
import React from 'react';
function PrimeCalculator() {
const [selectedNum, setSelectedNum] = React.useState(100);
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}
return (
<>
<form>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
// 為了防止電腦燒起來,我們限制一下傳入的值最大為 100k
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
There are {allPrimes.length} prime(s) between 1 and {selectedNum}:
{' '}
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}
function isPrime(n){
const max = Math.ceil(Math.sqrt(n));
if (n === 2) {
return true;
}
for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}
return true;
}
export default PrimeCalculator;
// Clock.js
import React from 'react';
import format from 'date-fns/format';
function Clock() {
const time = useTime();
return (
<p className="clock">
{format(time, 'hh:mm:ss a')}
</p>
);
}
import React from 'react';
import format from 'date-fns/format';
function Clock({ time }) {
return (
<p className="clock">
{format(time, 'hh:mm:ss a')}
</p>
);
}
export default Clock;
我將之前的例子抽離為了兩個單獨的組件 Clock 和 PrimeCalculator,從 App 組件抽離出來后,這兩個組件各自維護自己的狀態(tài)數(shù)據(jù),即使其中一個組件重新渲染了也不會影響另外一個。
這里我們使用 React.memo 包裹著組件保護它不受到無關(guān)狀態(tài)更新的影響。只有在 PurePrimeCalculator 只會在收到新數(shù)據(jù)或內(nèi)部狀態(tài)發(fā)生變化時重新渲染。這種組件被稱為 純組件。本質(zhì)上,我們告訴 React 這個組件在 給定相同輸入的情況下總是會產(chǎn)生相同的輸出 ,并且我們可以跳過沒有 props 和狀態(tài)改變的重渲染。
在上例中我們將組件引入 App.tsx 后再通過
React.memo進行包裹,在實際開發(fā)中我們更多的是在組件 export 的時候就使用React.memo進行包裹,這樣可以保證組件一直是純組件。上例只是為了更加清楚的在 App.tsx 中展示所有內(nèi)容。
這里有一個有趣的視角轉(zhuǎn)變: 在前面的例子中,我們是緩存了計算質(zhì)數(shù)的結(jié)果。然而在重構(gòu)后,我們已經(jīng)緩存了了整個組件。但無論使用哪種方式,昂貴的計算操作只有在 selectedNum 的值改變時才會執(zhí)行了,這里兩種方法沒有優(yōu)劣之分,根據(jù)實際情境來使用即可。
但在實際開發(fā)中你可能會發(fā)現(xiàn) 純組件也經(jīng)常發(fā)生重新渲染,即便它并沒有發(fā)生什么改變。接下來就為大家介紹可以使用 useMemo 來解決的第二種場景。
2. 引用保留
在下面的示例中,我們創(chuàng)建了一個 Boxes 組件用于展示幾個不同顏色的容器,純粹是用于裝飾。然后我們還定義了一個跟 Boxes 組件沒啥關(guān)系的 user's name 變量。
// App.jsx
import React from 'react';
import Boxes from './Boxes';
function App() {
const [name, setName] = React.useState('');
const [boxWidth, setBoxWidth] = React.useState(1);
const id = React.useId();
// Try changing some of these values!
const boxes = [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
return (
<>
<Boxes boxes={boxes} />
<section>
<label htmlFor={`${id}-name`}>
Name:
</label>
<input
id={`${id}-name`}
type="text"
value={name}
onChange={(event) => {
setName(event.target.value);
}}
/>
<label htmlFor={`${id}-box-width`}>
First box width:
</label>
<input
id={`${id}-box-width`}
type="range"
min={1}
max={5}
step={0.01}
value={boxWidth}
onChange={(event) => {
setBoxWidth(Number(event.target.value));
}}
/>
</section>
</>
);
}
export default App;
//Boxes.jsx
import React from 'react';
function Boxes({ boxes }) {
return (
<div className="boxes-wrapper">
{boxes.map((boxStyles, index) => (
<div
key={index}
className="box"
style={boxStyles}
/>
))}
</div>
);
}
export default React.memo(Boxes);
效果如下圖:

我們使用了 React.memo 包裹著 Boxes 組件,使它成為一個純組件,這說明只有在 props 更改時它才會重新渲染
然而實際使用時你會發(fā)現(xiàn),當用戶輸入 Name 時,Boxes 也會重新渲染。這時候你可能會好奇,有沒有搞錯?!為什么我們的 React.memo() 沒有在這里保護我們的組件?
Boxes 組件只有 1 個 prop boxes,看似我們在每次渲染時都為其提供了完全相同的數(shù)據(jù)。它每次渲染也總是一樣的:一個紅色的盒子,一個寬紫色的盒子,一個黃色的盒子。我們確實有一個 boxWidth 會影響 boxes 數(shù)組的狀態(tài)變量,但我們沒有改變它!
問題在于每次 React 重新渲染時,都會重新產(chǎn)生一個 boxes 數(shù)組,這個數(shù)組的值雖然每一次重新渲染都是相同的,但是它的 引用 卻是不同的。
這里暫時拋開 React 單純討論 JavaScript 可能比較好理解,讓我們看一個類似的例子:
function getNumbers() {
return [1, 2, 3];
}
const firstResult = getNumbers();
const secondResult = getNumbers();
console.log(firstResult === secondResult);
你怎么看?firstResult 等于 secondResult ? 從某種意義上說,它們是相同的。因為兩個變量具有相同的結(jié)構(gòu)[1, 2, 3]。但這不是 === 操作符實際判斷的標準。相反,=== 判斷的是兩個表達式 是否完全相同。
我們創(chuàng)建了兩個不同的數(shù)組。它們可能包含相同的內(nèi)容,但它們不是同一個數(shù)組,就像 兩個同卵雙胞胎不是同一個人一樣。

每次我們調(diào)用 getNumbers 函數(shù)時都會創(chuàng)建一個全新的數(shù)組,一個保存在計算機內(nèi)存中的獨特數(shù)組。如果我們多次調(diào)用它,我們將在內(nèi)存中存儲該數(shù)組的多個副本。
請注意,簡單的數(shù)據(jù)類型比如 字符串、數(shù)字和布爾值 可以通過值進行比較。但是當涉及到數(shù)組和對象時,它們只能通過引用進行比較。這部分內(nèi)容大家可以參考其他講引用類型的文章,這里不詳細展開。
回到 React, 我們的 Boxs React 組件也是一個 JavaScript 函數(shù)。當我們渲染它時,我們調(diào)用以下函數(shù):
// 每次渲染組件都會調(diào)用 App 函數(shù)
function App() {
// ...創(chuàng)建一個全新的數(shù)組...
const boxes = [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
// ...然后將數(shù)組作為 prop 傳入組件!
return (
<Boxes boxes={boxes} />
);
}
當 name 狀態(tài)更改時,我們的 App 組件將重新渲染,該組件將重新運行所有代碼,并構(gòu)建一個全新的 boxes 數(shù)組,并將其傳遞到 Boxes 組件。此時 Boxes 組件重新渲染,因為我們給了它一個全新的數(shù)組!
boxes 數(shù)組的結(jié)構(gòu)在不同的渲染之間雖然沒有變化,但是這不相關(guān)。React 只知道 Boxes 組件 prop 收到了一個新創(chuàng)建的,從未見過的數(shù)組。
為了解決這個問題,我們可以使用 useMemo hook:
const boxes = React.useMemo(() => {
return [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
}, [boxWidth]);
這里不像我們之前的例子,相比于質(zhì)數(shù),這里我們不需要擔心計算的代價。我們的唯一目標是保留對特定數(shù)組的引用。我們將 boxWidth 列為一個依賴項,因為我們確實希望在用戶調(diào)整紅色框的寬度時重新渲染 Box 組件。
這里有一個圖可以幫助你理解。在此之前,我們創(chuàng)建了一個全新的數(shù)組,作為每張快照的一部分:

然而通過 useMemo 我們復(fù)用了一個之前創(chuàng)建的 boxes 數(shù)組。

通過在多次渲染中保留相同的引用,我們允許純組件以我們想要的方式運作,忽略掉那些不影響用戶界面的渲染。
useCallback hook
好不容易介紹完了 useMemo,那么 useCallback 呢?
簡單概括:useMemo 和 useCallback 是一個東西,只是將返回值從 數(shù)組/對象 替換為了 函數(shù)。
函數(shù)是與數(shù)組和對象類似,都是通過引用而不是通過值進行比較的:
const functionOne = function() {
return 5;
};
const functionTwo = function() {
return 5;
};
console.log(functionOne === functionTwo); // false
這意味著如果我們在組件中定義一個函數(shù),它將在每個渲染中重新生成,每次生成一個相同但是唯一的函數(shù)。
讓我們看一個例子:
//App.jsx
import React from 'react';
import MegaBoost from './MegaBoost';
function App() {
const [count, setCount] = React.useState(0);
function handleMegaBoost() {
setCount((currentValue) => currentValue + 1234);
}
return (
<>
Count: {count}
<button
onClick={() => {
setCount(count + 1)
}}
>
Click me!
</button>
<MegaBoost handleClick={handleMegaBoost} />
</>
);
}
export default App;
// MegaBoost.jsx
import React from 'react';
function MegaBoost({ handleClick }) {
console.log('Render MegaBoost');
return (
<button
className="mega-boost-button"
onClick={handleClick}
>
MEGA BOOST!
</button>
);
}
export default React.memo(MegaBoost);
效果如圖:

這段代碼寫了一個經(jīng)典的計數(shù)器 app,但是帶有一個特殊的 “Mega Boost” 按鈕。點擊按鈕會大量增加計數(shù),以防您趕時間并且不想多次單擊標準按鈕。
由于使用了 React.memo 進行包裹, MegaBoost 組件是純組件,它雖然不依賴于 count ……但它會在更改時重新渲染 count!
就像我們在前面 boxes 數(shù)組中看到的那樣,這里的問題是我們在每次渲染時都生成了一個全新的函數(shù)。如果我們渲染 3 次,我們將創(chuàng)建 3 個獨立 handleMegaBoost 的函數(shù),突破 React.memo 的保護。
如果使用我們前面所學(xué)到的 useMemo,我們可以解決這樣的問題:
const handleMegaBoost = React.useMemo(() => {
return function() {
setCount((currentValue) => currentValue + 1234);
}
}, []);
這里不是返回一個數(shù)組,而是返回一個 函數(shù)。然后將該函數(shù)存儲在 handleMegaBoost 變量中。
這種寫法雖然也可以,但是有一種更好的方法:
const handleMegaBoost = React.useCallback(() => {
setCount((currentValue) => currentValue + 1234);
}, []);
useCallback 的用途與 useMemo 相同,但它是專門為函數(shù)構(gòu)建的。我們直接給返回它一個函數(shù),它會記住這個函數(shù),在渲染之間線程化它。
換句話說就是以下的兩種實現(xiàn)方式的效果是相同的:
React.useCallback(function helloWorld(){}, []);
// ...功能相當于:
React.useMemo(() => function helloWorld(){}, []);
useCallback 是一種語法糖,它的存在存粹是為了讓我們在緩存回調(diào)函數(shù)的時候可以方便點。
當使用這些 Hook 時
好了,我們已經(jīng)學(xué)習(xí)了 useMemo 和 useCallback 時如何允許我們在多次渲染之間線程化引用,以復(fù)用復(fù)雜的計算或者避免破壞純組件。
但還有一個問題是: 我們應(yīng)該在什么情況下使用這兩個 Hook ?
在我個人看來,將每個對象/數(shù)組/函數(shù)包裝在這些 hook 是在浪費時間。在大多數(shù)情況下,這些優(yōu)化的好處幾乎可以忽略不計; 因為 React 內(nèi)部是高度優(yōu)化的,并且 重新渲染通常并不像我們通常認為的那樣慢或昂貴!
使用這些 hook 的最佳方法是響應(yīng)問題。如果你注意到你的 app 變得有些遲鈍,你可以使用 React Profiler 來尋找慢速渲染。在某些情況下,可以通過重構(gòu) app 來提高性能。在其他情況下,useMemo 和 useCallback 可以幫助加快速度。
也就是說,在某些情況下,我確實會先發(fā)制人地應(yīng)用這些 hook。
未來可能會發(fā)生的改變
React 團隊正在積極研究是否有可能在編譯步驟中 “自動緩存” 代碼。雖然它仍然處于研究階段,但是通過早期的實驗看起來很有希望。
也許在未來這些優(yōu)化 React 都會為我們提前做好,但在此之前我們還是得自己去做一些優(yōu)化
要了解更多信息,可以看看黃玄的這個演講 “React without memo”[2]
通用自定義 hook
我最喜歡的自定義 hook 之一是 useToggle,這是一個友好的助手,其工作方式幾乎與 useState 完全相同,但只能在 true 和 false 之間切換狀態(tài)變量:
function App() {
const [isDarkMode, toggleDarkMode] = useToggle(false);
return (
<button onClick={toggleDarkMode}>
Toggle color theme
</button>
);
}
這里是這個自定義 hook 的代碼實現(xiàn):
function useToggle(initialValue) {
const [value, setValue] = React.useState(initialValue);
const toggle = React.useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
注意這里的 toggle 函數(shù)使用了 useCallback 進行緩存。
當咱們構(gòu)建這樣的自定義可復(fù)用 hook 時,我希望使它們盡可能高性能,因為我不知道它們將來在哪里使用。在95% 的情況下,這可能是過度封裝的,但是如果我使用這個 hook 30 或 40 次,這將很有可能有助于提高我們的 app 的性能。
內(nèi)部 context providers
當我們通過 context 在組件之間共享數(shù)據(jù)時,通常會傳遞一個大的對象作為 value 屬性。
一般來說,將這個對象緩存起來是個好方法:
const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }){
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
};
}, [user, status, forgotPwLink]);
return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
);
}
這樣寫有什么好處呢?因為可能有幾十個純組件使用這個 context 。如果沒有使用 useMemo,那么當 AuthProvider 的父組件恰好重新渲染時,這些使用 context 組件都將被迫重新渲染。
React 的樂趣
恭喜你看到了這里,我知道這里面可能有些內(nèi)容你從未了解過。這兩個 hook 確實是比較棘手,畢竟 React 本身就是龐大且復(fù)雜的,是一個上手難度比較高的工具!
但事實是: 如果你能克服最初的困難,使用 React 絕對是一種樂趣。
我從 2015 年開始使用 React,它已經(jīng)成為我最喜歡的構(gòu)建復(fù)雜用戶界面和 Web 應(yīng)用程序的方式。我已經(jīng)嘗試了幾乎所有的 JS 框架,但是我覺得它們的效率都不如 React 的效率。
如果你覺得這篇博客文章哪怕只有一點點幫助,你都會從中學(xué)到很多東西。
參考資料
https://www.joshwcomeau.com/react/usememo-and-usecallback/: https://www.joshwcomeau.com/react/usememo-and-usecallback/
[2]https://www.youtube.com/watch?v=lGEMwh32soc: https://www.youtube.com/watch?v=lGEMwh32soc
Node 社群 我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點贊、在看” 支持一波??
