前言
大家好耻陕,我是麥西。
近來發(fā)現(xiàn) React 官方文檔更新了夷家。
仔細想來绒疗,學(xué)習(xí)使用 React 這么久還沒有好好拜讀過官方文檔。于是認真讀寫了一遍官方教程腺毫。這里把學(xué)到的一些知識記錄下來癣疟,分享給大家。
純函數(shù)組件
React 官方推薦全面擁抱 hooks潮酒。這也就意味著睛挚,類組件已經(jīng)是過去式了。這一點從官方文檔也可以看出急黎,新的官方文檔已經(jīng)不再介紹和使用類組件了扎狱。
部分 JavaScript 函數(shù)是存粹的,這類函數(shù)被稱為純函數(shù)勃教。
純函數(shù)通常具有以下特征:
- 只負責(zé)自己的工作淤击。它不會更改函數(shù)調(diào)用前就存在的對象或變量。
- 輸入相同故源,則輸出相同遭贸。給定相同的輸入,純函數(shù)總是返回相同的結(jié)果心软。
可簡單的理解為壕吹,函數(shù)的執(zhí)行不依賴且不改變外界。純函數(shù)的優(yōu)點是沒有副作用删铃,可移植性好耳贬。在 A 項目能夠用,B 項目想要使用直接拿過來就好了猎唁≈渚ⅲ可以通過下面這幾個例子感受下純函數(shù)的概念:
// 純函數(shù)
function add(a, b) {
return a + b;
}
// 非純函數(shù),函數(shù)執(zhí)行依賴外界變量all
let all = 100;
function plus(a) {
return all + a;
}
// 非純函數(shù),函數(shù)執(zhí)行改變了外界變量obj
let obj = {};
function fun(a) {
obj.a = a;
return a;
}
// 非純函數(shù)腐魂,函數(shù)的執(zhí)行依賴外界getCount()
async function(a, b) {
const c = await getCount(); // 副作用
return a + b +c;
}
// addConst是否是純函數(shù)存在爭議帐偎,我更傾向于它是
const data = 100;
function addConst(a) {
return a + data;
}
最后一個例子,addConst
依賴于 data
, 但 data
是常量蛔屹。這種情況存在爭議削樊,有人認為是,也有人認為不是兔毒。我更傾向于addConst
是純函數(shù)漫贞。
官方建議建議我們使用純函數(shù)來編寫組件。非純函數(shù)編寫的組件可能會存在副作用育叁,造成意料之外的影響迅脐。下面是一個非純函數(shù)組件的例子:
let guest = 0;
function Cup() {
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
頁面顯示的結(jié)果是:
Tea cup for guest #1
Tea cup for guest #2
Tea cup for guest #3
在上面這個例子中,Cup
是非純函數(shù)組件豪嗽,它依賴于外界 guest
變量谴蔑。由于多個 Cup
組件依賴的是同一個變量guest
。當(dāng)我們每次使用組件的時候龟梦,都會修改guest
树碱,這就會導(dǎo)致每次使用組件都會產(chǎn)生不同的結(jié)果。
因此变秦,為了避免出現(xiàn)意想不到的結(jié)果成榜,我們最好使用純函數(shù)編寫組件。
渲染和提交
在 React 應(yīng)用中一次屏幕更新都會發(fā)生以下三個步驟:
1. 觸發(fā)
也就說觸發(fā)一次渲染蹦玫。有兩種原因會導(dǎo)致組件渲染:
-
組件的初次渲染: 當(dāng)應(yīng)用啟動時赎婚,會觸發(fā)初次渲染。也就是
render
方法的執(zhí)行樱溉。
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<Image />); // 初次渲染
- 組件或者其祖先的狀態(tài)發(fā)生了改變
一旦組件被初始渲染后挣输,我們可以通過 set函數(shù)
更新組件狀態(tài)來觸發(fā)之后的渲染。
2. 渲染
在我們觸發(fā)渲染后福贞,React 會調(diào)用組件來確定要在屏幕上顯示的內(nèi)容撩嚼。渲染中
即 React 在調(diào)用你的組件函數(shù)。
在進行初次渲染時, React 會調(diào)用根組件挖帘。
對于后續(xù)的渲染, React 會調(diào)用 內(nèi)部狀態(tài)更新 觸發(fā)了渲染 的函數(shù)組件完丽。
3. 提交
在渲染(調(diào)用)您的組件之后,React 將會修改 DOM拇舀。
對于初次渲染逻族, React 會使用
appendChild()
DOM API 將其創(chuàng)建的所有 DOM 節(jié)點放在屏幕上。-
對于再次渲染骄崩, React 將應(yīng)用最少的必要操作(在渲染時計算)聘鳞,以使得 DOM 與最新的渲染輸出相互匹配薄辅。
React 僅在渲染之間存在差異時才會更改 DOM 節(jié)點。 如果渲染結(jié)果與上次一樣抠璃,那么 React 將不會修改 DOM站楚。
在渲染完成并且 React 更新 DOM 之后,瀏覽器就會重新繪制屏幕搏嗡。
useState
使用 state 需要注意以下幾點:
當(dāng)一個組件需要在多次渲染記住某些信息時窿春,使用 state 變量。
調(diào)用 Hook 時彻况,包括 useState谁尸,僅在組件或者另一個 Hook 的頂層作用域調(diào)用舅踪。
state 是隔離且私有的纽甘。也就是說,將一個組件調(diào)用兩次抽碌,他們內(nèi)部的 state 不會互相影響悍赢。
1. state 如同一張快照
當(dāng) React 重新渲染一個組件時:
- React 會再次調(diào)用你的函數(shù)
- 你的函數(shù)會返回新的 JSX
- React 會更新界面來匹配你返回的 JSX
作為一個組件的記憶,state 不同于在你的函數(shù)返回之后就會消失的普通變量货徙。state 實際上“活”在 React 本身中——就像被擺在一個架子上左权!——位于你的函數(shù)之外。當(dāng) React 調(diào)用你的組件時痴颊,它會為特定的那一次渲染提供一張 state 快照赏迟。你的組件會在其 JSX 中返回一張包含一整套新的 props 和事件處理函數(shù)的 UI 快照 ,其中所有的值都是 根據(jù)那一次渲染中 state 的值 被計算出來的蠢棱!
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>
+3
</button>
</>
);
}
以下是這個按鈕的點擊事件處理函數(shù)通知 React 要做的事情:
-
setNumber(number + 1)
:number
是 0 所以setNumber(0 + 1)
锌杀。
React 準(zhǔn)備在下一次渲染時將number
更改為 1。 -
setNumber(number + 1)
:number
是 0 所以setNumber(0 + 1)
泻仙。
React 準(zhǔn)備在下一次渲染時將number
更改為 1糕再。 -
setNumber(number + 1)
:number
是 0 所以setNumber(0 + 1)
。
React 準(zhǔn)備在下一次渲染時將number
更改為 1玉转。
盡管你調(diào)用了三次 setNumber(number + 1)
突想,但在這次渲染的 事件處理函數(shù)中 number
會一直是 0,所以你會三次將 state 設(shè)置成 1究抓。這就是為什么在你的事件處理函數(shù)執(zhí)行完以后猾担,React 重新渲染的組件中的 number
等于 1 而不是 3。
為了更好理解刺下,我們看下面這個例子:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>
+5
</button>
</>
);
}
點擊+5 后垒探,彈出的數(shù)字是 0,而不是 5. 點擊按鈕后的操作:
setNumber(0+5)
js setTimeout(() => { alert(0) })
2. 將 state 加入隊列
React 會對 state 更新進行批處理怠李。在上面的示例中圾叼,連續(xù)調(diào)用了三次setNumber(number + 1)
并不能得到我們想要的結(jié)果蛤克。
React 會等到事件處理函數(shù)中的所有代碼都運行完畢再處理你的 state 更新。
我們可以通過更新函數(shù)來在下次渲染之前多次更新同一個 state夷蚊。比如:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);
}}>
+3
</button>
</>
);
}
下面是 React 在執(zhí)行事件處理函數(shù)時處理這幾行代碼的過程:
-
setNumber(n => n + 1)
:n => n + 1
是一個函數(shù)构挤。React 將它加入隊列。 -
setNumber(n => n + 1)
:n => n + 1
是一個函數(shù)惕鼓。React 將它加入隊列筋现。 -
setNumber(n => n + 1)
:n => n + 1
是一個函數(shù)。React 將它加入隊列箱歧。
當(dāng)你在下次渲染期間調(diào)用 useState 時矾飞,React 會遍歷隊列。之前的 number state 的值是 0呀邢,所以這就是 React 作為參數(shù) n 傳遞給第一個更新函數(shù)的值洒沦。然后 React 會獲取你上一個更新函數(shù)的返回值,并將其作為 n 傳遞給下一個更新函數(shù)价淌,以此類推:
更新隊列 | n | 返回值 |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
看看下面這個例子:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
setNumber(42);
}}>
增加數(shù)字
</button>
</>
);
}
以下是 React 在執(zhí)行事件處理函數(shù)時處理這幾行代碼的過程:
-
setNumber(number + 5)
:number
為 0申眼,所以setNumber(0 + 5)
。React 將 “替換為 5” 添加到其隊列中蝉衣。 -
setNumber(n => n + 1)
:n => n + 1
是一個更新函數(shù)括尸。React 將該函數(shù)添加到其隊列中。 -
setNumber(42)
:React 將 “替換為 42” 添加到其隊列中病毡。
在下一次渲染期間濒翻,React 會遍歷 state 隊列:
更新隊列 | n | 返回值 |
---|---|---|
替換為 5 | 0 | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
替換為 42 | 6 | 42 |
可以這樣來理解狀態(tài)隊列的更新:
function getFinalState(baseState, queue) {
let finalState = baseState;
queue.forEach((update) => {
finalState = typeof update === 'function' ? update(finalState) : update;
});
return finalState;
}
其中 baseState 是初始狀態(tài),queue 是狀態(tài)更新隊列啦膜,包括數(shù)據(jù)和更新函數(shù)有送。
3. set 函數(shù)一定會觸發(fā)更新嗎?
看下面這個例子:
export default function Counter() {
const [number, setNumber] = useState(0);
const [person, setPerson] = useState({ name: 'jack' });
console.log('渲染');
return (
<>
<button
onClick={() => {
setNumber(number);
}}>
增加數(shù)字
</button>
<h1>{number}</h1>
<button
onClick={() => {
person.age = 18;
setPerson(person);
}}>
修改對象
</button>
<h1>{JSON.stringify(person)}</h1>
</>
);
}
組件的更新意味著組件函數(shù)的重新執(zhí)行功戚。對于上面這個例子娶眷,無論是點擊 增加數(shù)字
還是 改變對象
都沒有打印 渲染
。
set 函數(shù)觸發(fā)更新的條件:
- 值類型啸臀,state 的值改變
- 引用類型届宠,state 的引用改變
對于上面的例子:
- number 是值類型。點擊增加數(shù)字乘粒,值沒有改變豌注,不會觸發(fā)更新。
- person 是引用類型灯萍。點擊修改對象轧铁,雖然 person 對象的值雖然變化了,但是引用地址沒有變化旦棉,因此也不會觸發(fā)更新齿风。
4. 構(gòu)建 state 的原則
- 合并關(guān)聯(lián)的 state
有時候我們可能會不確定使用單個 state 還是多個 state 變量药薯。
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或
const [position, setPosition] = useState({ x: 0, y: 0 });
從技術(shù)上講,我們可以使用其中任何一種方法救斑。但是童本,如果某兩個 state 變量總是一起變化,則將它們統(tǒng)一成一個 state 變量可能更好脸候。這樣你就不會忘記讓它們始終保持同步穷娱。
- 避免矛盾的 state
import { useState } from 'react';
export default function Send() {
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
return (
<>
<button
onClick={() => {
setIsSent(false);
setIsSending(true);
setTimeout(() => {
setIsSending(false);
setIsSent(true);
}, 2000);
}}>
發(fā)送
</button>
{isSending && <h1>正在發(fā)送...</h1>}
{isSent && <h1>發(fā)送完成</h1>}
</>
);
}
盡管這段代碼是有效的,但也會讓一些 state “極難處理”运沦。例如泵额,如果你忘記同時調(diào)用 setIsSent
和 setIsSending
,則可能會出現(xiàn) 二者 同時為 true 的情況携添。
可以用一個 status 變量來代替它們嫁盲。代碼如下:
import { useState } from 'react';
export default function Send() {
const [status, setStatus] = useState('init');
return (
<>
<button
onClick={() => {
setStatus('sending');
setTimeout(() => {
setStatus('sent');
}, 2000);
}}>
發(fā)送
</button>
{status === 'sending' && <h1>正在發(fā)送...</h1>}
{status === 'sent' && <h1>發(fā)送完成</h1>}
</>
);
}
- 避免冗余的 state
import { useState } from 'react';
export default function Name() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
return (
<>
<span>First Name</span>
<input
value={firstName}
onChange={(e) => {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}}
/>
<span>Last Name</span>
<input
value={lastName}
onChange={(e) => {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}}
/>
<h1>{fullName}</h1>
</>
);
}
能夠看出,fullName 是冗余的 state薪寓。我們可以直接:
const fullName = firstName + ' ' + lastName;
無需再把 fullName 存放到 state 中亡资。
- 避免重復(fù)的 state
有時候澜共,在我們存儲的 state 中向叉,可能有兩個 state 有重合的部分。這時候我們就要考慮是不是有重復(fù)的問題了嗦董。
具體例子見這里
5. 保存和重置 state
前面我們說過母谎,組件內(nèi)部的 state 是互相隔離的。一個組件 state 的改變不會影響另外一個京革。然而奇唤,我們看下面這個例子:
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
<label>
<input
type='checkbox'
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
使用好看的樣式
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)}>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>加一</button>
</div>
);
}
當(dāng)我們修改了 Counter
組件 的 state 后,點擊 checkbox
切換到另一個 Counter
匹摇,舊 Counter
的 state 并沒有變?yōu)?0咬扇,而是保留了下來。如下圖:
在 React 中廊勃,相同位置的相同組件會使得 state 保留下來懈贺。那么怎么才能讓上述例子的 state 重置呢?
有兩種方法:
1. 將組件渲染在不同的位置
{
isFancy ? (
<Counter isFancy={true} />
) : (
<div>
<Counter isFancy={false} />
</div>
);
}
2. 使用 key 來標(biāo)識組件
{
isFancy ? <Counter isFancy={true} key={fancyTrue} /> : <Counter isFancy={false} key={fancyFalse} />;
}
效果如下:
useRef
基本用法
提起 useRef
坡垫,很多人都會把它跟 DOM 聯(lián)系起來梭灿。其實 useRef 不止可以用來存儲 DOM 元素。它的定義是:
如果希望組件記住某些信息冰悠,但又不想讓這些信息觸發(fā)新的渲染堡妒,可以使用 useRef。
比如下面這個例子:
import React, { useState, useEffect } from 'react';
let timer = null;
function Counter() {
const [count, setCount] = useState(0);
const onStart = () => {
timer = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const onStop = () => {
clearInterval(timer);
};
useEffect(() => {
return () => {
clearInterval(timer);
};
}, []);
return (
<>
<h1>{count}</h1>
<button onClick={onStart}>開始</button>
<button onClick={onStop}>停止</button>
</>
);
}
這個例子里溉卓,我們寫了一個 Counter
組件皮迟。點擊開始按鈕 count
每秒增加 1搬泥,點擊停止按鈕 count
停止增加。
看上去伏尼,這個組件好像很 OK佑钾。
但是如果在一個頁面使用 Counter 組件兩次,就會發(fā)現(xiàn)烦粒,第一個定時器停止不了休溶。
export default function App() {
return (
<div className='App'>
<Counter />
<Counter />
</div>
);
}
如下圖:
這是因為兩個組件公用同一個 timer
變量,第二個組件修改 timer
后扰她,導(dǎo)致第一個組件中 clearInterval
處理的是第二個組件的 timer
兽掰。 因此第一個組件無法停止定時增加。
官網(wǎng)推薦我們使用純函數(shù)編寫組件也是基于此徒役。
估計有人會說孽尽,我可以把 timer
變量放到 Counter
內(nèi)部。組件內(nèi)部的變量是互相隔離的, 這樣就不會把第一個 Counter 組件的 timer
給覆蓋了忧勿。
放到內(nèi)部有兩種情況:
1. 直接使用變量杉女。 當(dāng)組件更新的時候,組件函數(shù)重新執(zhí)行鸳吸,會導(dǎo)致 timer
重新創(chuàng)建熏挎,因此并不能清除之前的 timer
。
2. 使用 state晌砾。 使用 state 可以解決問題坎拐,但是會導(dǎo)致不必要的渲染。每次 timer
變化都會導(dǎo)致組件重新渲染养匈。
其實對于這種定時器清理的問題哼勇,我們可以使用 useRef。useRef 創(chuàng)建一個變量呕乎,變量里有一個 current 屬性积担。
const timeRef = useRef(null);
比如上面這段代碼,會創(chuàng)建一個變量 timeRef, 它的結(jié)構(gòu)類似 { current: null }
猬仁。
使用 ref 修改上述例子中的代碼:
function Counter() {
const [count, setCount] = useState(0);
const timeRef = useRef(null);
const onStart = () => {
timeRef.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const onStop = () => {
clearInterval(timeRef.current);
};
useEffect(() => {
return () => {
clearInterval(timeRef.current);
};
}, []);
return (
<>
<h1>{count}</h1>
<button onClick={onStart}>開始</button>
<button onClick={onStop}>停止</button>
</>
);
}
運行試試帝璧,完美解決了之前的問題。
可以這樣理解逐虚,ref 跟 state 的區(qū)別是聋溜,ref 不會導(dǎo)致組件重新渲染。
使用 ref 操作 DOM
我們來看一個 ref 操作 DOM 的例子:
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>聚焦輸入框</button>
</>
);
}
效果:點擊聚焦輸入框按鈕叭爱,輸入框?qū)劢埂?/p>
這段代碼主要做了以下事情:
- 使用
useRef
Hook 聲明inputRef
撮躁。 -
<input ref={inputRef}>
告訴 React 將這個 input 的 DOM 節(jié)點放入inputRef.current
。 - 在
handleClick
函數(shù)中买雾,從inputRef.current
讀取 input DOM 節(jié)點并調(diào)用它的focus()
把曼。 - 給按鈕添加點擊事件
handleClick
forwardRef
我們可以使用 ref 屬性配合 useRef 直接調(diào)用 DOM杨帽。那么可不可以給組件添加 ref 調(diào)用組件的 DOM 呢?讓我們來試一下:
import { useRef } from 'react';
function MyInput(props) {
return <input {...props} />;
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>聚焦輸入框</button>
</>
);
}
我們給 MyInput
組件加上了 ref
嗤军,可是當(dāng)我們點擊 聚焦輸入框按鈕注盈,則會報錯:Cannot read properties of null (reading 'focus')
也就是說 inputRef.current
是 null
。我們并不能拿到組件的 DOM 元素叙赚。
默認情況下老客,React 不允許組件訪問其他組件的 DOM 節(jié)點。這是因為 ref 是應(yīng)急方案震叮,應(yīng)當(dāng)謹(jǐn)慎使用胧砰。如果組件想要暴露自己的的 DOM,則需要使用forwardRef
來包裝苇瓣,并把 ref 轉(zhuǎn)發(fā)給自己的子元素尉间。 比如這樣:
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>聚焦輸入框</button>
</>
);
}
它是這樣工作的:
<MyInput ref={inputRef} />
告訴 React 將對應(yīng)的 DOM 節(jié)點放入inputRef.current
中。但是击罪,這取決于MyInput
組件是否允許這種行為哲嘲, 默認情況下是不允許的。MyInput
組件是使用 forwardRef 聲明的媳禁。 這讓從上面接收的inputRef
作為第二個參數(shù) ref 傳入組件眠副,第一個參數(shù)是 props 。MyInput
組件將自己接收到的 ref 傳遞給它內(nèi)部的<input>
损话。
這樣就通過 forwardRef
向父組件暴露了子組件的 DOM 節(jié)點侦啸。
useEffect
useEffect 是使用頻率僅低于 useState 的 hook槽唾。很多人把 useEffect 當(dāng)做監(jiān)聽器來使用丧枪。這是不太妥當(dāng)?shù)摹?/p>
useEffect 是用來處理由渲染本身而不是點擊事件引起的副作用。
基本用法
useEffect(setup, dependencies?)
setup
處理邏輯庞萍,是一個函數(shù)拧烦。可以返回一個清理函數(shù)dependencies
是依賴項钝计,當(dāng)依賴項變化會執(zhí)行, 會執(zhí)行setup
函數(shù)
值得注意的幾個問題
1. useEffect 的執(zhí)行時機
- useEffect 在組件掛載完成后恋博,也就是說 DOM 更新完畢后,按照定義的順序執(zhí)行私恬。
比如這個例子:
import React, { useState, useEffect } from 'react';
export default function App() {
const [number, setNumber] = useState(0);
useEffect(() => {
console.log('-- 空依賴1债沮,useEffect執(zhí)行 --');
}, []);
useEffect(() => {
console.log('-- 非空依賴,useEffect執(zhí)行 --', number);
}, [number]);
useEffect(() => {
console.log('-- 空依賴2本鸣,useEffect執(zhí)行 --');
}, []);
console.log('渲染');
return (
<>
<h1>{number}</h1>
</>
);
}
結(jié)果打印為:
渲染
-- 空依賴1疫衩,useEffect執(zhí)行 --
-- 非空依賴,useEffect執(zhí)行 -- 0
-- 空依賴2荣德,useEffect執(zhí)行 --
執(zhí)行順序依次為:
所有 DOM 更新完畢 => 空依賴 1 useEffect 執(zhí)行 => 非空依賴 useEffect 執(zhí)行 => 空依賴 2 useEffect 執(zhí)行
- useEffect 的清理函數(shù)在組件卸載期間調(diào)用或者下次運行之前調(diào)用闷煤。 比如下面這個例子:
import React, { useState, useEffect } from 'react';
function Title() {
const [title, setTitle] = useState('這里是標(biāo)題');
useEffect(() => {
console.log('空依賴童芹,useEffect執(zhí)行');
return () => console.log('空依賴,useEffect清理函數(shù)執(zhí)行');
}, []);
useEffect(() => {
console.log('非空依賴鲤拿,useEffect執(zhí)行');
return () => console.log('非空依賴假褪,useEffect清理函數(shù)執(zhí)行');
}, [title]);
return <h1 onClick={() => setTitle((title) => `${title}1`)}>{title}</h1>;
}
export default function App() {
const [titleVisible, setTitleVisible] = useState(true);
return (
<>
{titleVisible && <Title />}
<button onClick={() => setTitleVisible(!titleVisible)}>{`${titleVisible ? '隱藏' : '顯示'}標(biāo)題`}</button>
</>
);
}
由前面我們知道,組件掛載完成后才會按照順序執(zhí)行 useEffect, 因此打印結(jié)果是:
空依賴近顷,useEffect執(zhí)行
非空依賴生音,useEffect執(zhí)行
然后點擊標(biāo)題,會打又仙:
非空依賴久锥,useEffect清理函數(shù)執(zhí)行
非空依賴,useEffect執(zhí)行
最后异剥,我們點擊 隱藏標(biāo)題 按鈕瑟由,會打印:
空依賴冤寿,useEffect清理函數(shù)執(zhí)行
非空依賴歹苦,useEffect清理函數(shù)執(zhí)行
也就是說,空依賴的 useEffect 只會在組件掛載完成后執(zhí)行督怜,清理函數(shù)只會在組件卸載后執(zhí)行
非空依賴的 useEffect 則有兩種情況:
- 組件掛載完成后執(zhí)行殴瘦,清理函數(shù)在組件卸載后執(zhí)行
- 依賴發(fā)生變化時執(zhí)行,清理函數(shù)會在依賴發(fā)生變化号杠,useEffect 內(nèi)的邏輯執(zhí)行前調(diào)用
2. 依賴項
依賴項為空蚪腋,則只會在組件掛載完成后執(zhí)行一次。當(dāng)組件再次更新時候姨蟋,不會執(zhí)行屉凯。
如果 React 的所有依賴項都具有與上次渲染期間相同的值,則 React 將跳過 Effect
您不能“選擇”您的依賴項眼溶。它們由 Effect 中的代碼決定悠砚。
依賴項需要是能夠觸發(fā)組件更新的變量,比如 state 或者 props
不需要 effect 的情況
effect 是 React 的應(yīng)急方案堂飞。它允許我們能夠與一些外部系統(tǒng)同步灌旧,比如 ajax 請求和瀏覽器 DOM。如果不涉及外部系統(tǒng)绰筛,則不需要 effect枢泰。刪除不必要的 effect 可以使代碼更容易理解,運行速度更快并且不容易出錯铝噩。
下面是幾種常見的不需要 effect 的情況:
- 如果您可以在渲染期間計算某些東西衡蚂,則不需要 Effect。
- 要緩存昂貴的計算,請?zhí)砑?useMemo 而不是 useEffect.
- 要重置整個組件樹的狀態(tài)讳窟,請將不同的傳遞 key 給它让歼。
- 要重置特定位的狀態(tài)以響應(yīng)屬性更改,請在渲染期間設(shè)置它丽啡。
- 因為顯示組件而運行的代碼應(yīng)該在 Effects 中谋右,其余的應(yīng)該在事件中。
- 如果您需要更新多個組件的狀態(tài)补箍,最好在單個事件期間執(zhí)行改执。
- 每當(dāng)您嘗試同步不同組件中的狀態(tài)變量時,請考慮提升狀態(tài)坑雅。
- 您可以使用 Effects 獲取數(shù)據(jù)辈挂,但您需要實施清理以避免競爭條件。
具體例子可以參考官網(wǎng)https://react.docschina.org/learn/you-might-not-need-an-effect#caching-expensive-calculations
我的理解就是裹粤,盡可能少用 useEffect终蒂,除非不用不行的情況。
useLayoutEffect
useLayoutEffect 跟 useEffect 唯一的不同就是二者的執(zhí)行時機不同遥诉。
前面說過拇泣,對于一次更新有三個階段:觸發(fā)、渲染(render) 和 提交(commit)矮锈。
render 階段主要是組件函數(shù)執(zhí)行霉翔,jsx 轉(zhuǎn)化為 Fiber 等工作。
commit 階段主要是把更改反映到瀏覽器上苞笨,類似 document.appendChild()
之類的操作债朵。
useEffect 在 commit 階段完成后執(zhí)行。
useLayoutEffect 在 commit 階段之前執(zhí)行瀑凝。
由于 commit 階段主要是頁面更新的操作男娄,因此useLayoutEffect
會阻塞頁面更新恨樟。
比如這個例子:
import { useState, useEffect, useLayoutEffect } from 'react';
export default function App() {
const [text, setText] = useState('11111');
useEffect(() => {
console.log('useEffect');
let i = 0;
while (i < 100000000) {
i++;
}
setText('00000');
}, []);
// useLayoutEffect(() => {
// console.log("useLayoutEffect");
// let i = 0;
// while (i < 100000000) {
// i++;
// }
// setText("00000");
// }, []);
return <h1>{text}</h1>;
}
使用 useEffect
頁面會有明顯的從 11111
變成 00000
的過程奉狈。使用 useLayoutEffect
則不會央渣。
讓我們梳理下執(zhí)行流程:
useEffect: render => commit(反映到頁面上) => useEffect => render => commit(反映到頁面上)
useLayoutEffect: render => useLayoutEffect => render => commit(反映到頁面上)
useLayoutEffect
執(zhí)行后發(fā)現(xiàn) state 更新,就不再把 11111
反映到頁面上了射窒,直接再次執(zhí)行 react 渲染。因此我們沒有看到從 11111
閃爍成 00000
的過程将塑。
自定義 hook
自定義 hook 是一個函數(shù)脉顿,它允許我們在組件之間共享邏輯。
在使用自定義 hook 之前点寥,我們代碼復(fù)用的最小單元是組件艾疟。使用自定義 hook 之后,我們可以方便地復(fù)用組件里的邏輯。
基本使用
編寫自定義 hook 需要遵循以下規(guī)則:
命名必須是 use 后跟大寫字母蔽莱,比如
useLogin
,useForceUpdate
自定義 hook 中至少要使用一個其他 hook
比如我們寫一個強制刷新的 hook:
import { useState } from 'react';
function useForceUpdate() {
const [, setForceState] = useState({});
return () => setForceState({});
}
在組件里使用:
export default function App() {
const forceUpdate = useForceUpdate();
console.log('render');
return <button onClick={forceUpdate}>強制刷新</button>;
}
當(dāng)我們點擊 強制刷新
按鈕的時候弟疆,會打印 render。也就是App
組件重新渲染了盗冷。
何時使用
就個人理解怠苔,我覺得有兩種情況比較適合使用自定義 hook:
有經(jīng)常復(fù)用的組件邏輯時
使用自定義 hook 后能夠讓代碼邏輯,數(shù)據(jù)流向更清晰
memo
在 React 中仪糖,父組件的重新渲染會導(dǎo)致子組件的重新渲染柑司。memo 允許我們在 props 不變的情況下避免渲染子組件。
語法
memo(Component, arePropsEqual?)
:包裝一個組件锅劝,并獲得改組件的緩存版本攒驰。
Component
: 要包裝的組件。
arePropsEqual(prevProps, nextProps)
: 接收兩個參數(shù)故爵,前一次的 props 和后一次的 props玻粪。返回值是一個布爾類型,true
表示新舊 props 相等诬垂,false
表示兩次 props 不相等奶段。
下面用一個例子感受它的用法。
緩存子組件的例子
import React, { useState, memo } from 'react';
function Hello({ text }) {
console.log('子組件重新渲染');
return <h1>{`hello ${text}!`}</h1>;
}
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const onAddCount = () => {
setCount((count) => count + 1);
};
const onChangeText = () => {
setText('world');
};
return (
<>
<span>{count}</span>
<button onClick={onAddCount}>+1</button>
<Hello text={text} />
<button onClick={onChangeText}>改變子組件文本</button>
</>
);
}
當(dāng)我們點擊 +1
按鈕時剥纷,會打印 子組件重新渲染
痹籍。也就是說當(dāng)我們的父組件更新的時候,子組件也會相應(yīng)更新晦鞋。
但是如果我們用 memo 來包裹子組件蹲缠,代碼如下:
import React, { useState, memo } from 'react';
const Hello = memo(function Hello() {
console.log('子組件重新渲染');
return <h1>Hello, world!</h1>;
});
// ...
當(dāng)我們點擊 +1
按鈕時, 子組件重新渲染
不會再打印悠垛。也就說我們通過 memo
實現(xiàn)了子組件的緩存线定。
需要注意的是,當(dāng)上下文或者子組件內(nèi)部狀態(tài)變化的時确买,依然會觸發(fā)更新斤讥。 memo 緩存組件只是針對 props 不發(fā)生改變的情況。
prop 是對象湾趾、數(shù)組或函數(shù)的情況
當(dāng)傳遞給子組件的 prop 是對象芭商、數(shù)組或函數(shù)時,由于它們是引用類型搀缠,父組件重新渲染會導(dǎo)致它們被重新定義铛楣。也就是說,props 發(fā)生了變化艺普。這種情況下簸州,依然會觸發(fā)子組件更新鉴竭。
比如下面這個例子:
import React, { useState, memo } from 'react';
const List = memo(function List({ list }) {
console.log('子組件重新渲染');
return (
<>
{list.map((item) => (
<div key={item.id}>{item.content}</div>
))}
</>
);
});
export default function App() {
const [title, setTitle] = useState('父組件');
const [todoList, setTodoList] = useState([
{ id: 1, content: '吃飯', isDone: true },
{ id: 2, content: '睡覺', isDone: false },
{ id: 3, content: '洗澡', isDone: true },
{ id: 4, content: '刷牙', isDone: false },
{ id: 5, content: '刷抖音', isDone: false }
]);
const changeTitle = () => {
setTitle('父組件' + Math.random().toFixed(2));
};
const list = todoList.filter((item) => item.isDone);
return (
<>
<h1 onClick={changeTitle}>{title}</h1>
<List list={list} />
</>
);
}
點擊父組件,依然會觸發(fā)子組件渲染岸浑。這是由于每次父組件渲染都會重新定義一個變量 list
, 兩次的 list
不是同一個引用搏存。
這種情況要怎么處理才能避免子組件渲染呢?有兩種辦法:
1. 使用比較函數(shù)
我們可以給 memo 添加第二個參數(shù)arePropsEqual
:
// ...
(prevProps, nextProps) => {
return (
prevProps.list.length === nextProps.list.length &&
prevProps.list.every((item) => {
let allOk = true;
for (let key in item) {
if (prevProps[key] !== nextProps[key]) {
allOk = false;
}
}
return allOk;
})
);
};
// ...
這樣矢洲,當(dāng)修改 title 時璧眠,list 的內(nèi)容沒有變化,并不會觸發(fā)子組件更新兵钮。
個人建議蛆橡,盡可能避免使用比較函數(shù)。主要出于兩個考慮:一來別人需要閱讀你的比較函數(shù)來確定你的組件更新規(guī)則掘譬;二來我們重寫比較函數(shù)就意味著每次父組件更新都會執(zhí)行比較函數(shù)泰演。如果比較函數(shù)比較復(fù)雜且耗時,那么使用比較函數(shù)就不再是好的選擇了葱轩。
2. 使用 useCallback 或者 useMemo 來緩存引用類型
useCallback 用來緩存一個函數(shù)睦焕。在這個例子里,使用 useMemo 比較合適靴拱。
修改 list 的定義垃喊,代碼如下:
// ...
// 使用useMemo緩存list, 這樣title改變不會再觸發(fā)子組件渲染
const list = useMemo(() => todoList.filter((item) => item.isDone), [todoList]);
// ...
這樣,由于我們緩存了 list
, 當(dāng)修改 title
時袜炕,list
仍為同一個 list
本谜,并不會觸發(fā)子組件更新。
useMemo
useMemo 允許我們緩存一個計算結(jié)果偎窘。當(dāng)再次渲染的時候乌助,返回上一次的結(jié)果而不是重新計算。
語法
const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
: 緩存的計算結(jié)果陌知。 當(dāng)它是一個函數(shù)時他托,會緩存這個函數(shù)的返回值。dependencies
: 依賴項仆葡。當(dāng)依賴項變化時赏参,重新計算結(jié)果。
使用場景
- 防止組件重新渲染
比如前面的例子:
當(dāng) prop 是對象沿盅、數(shù)組或函數(shù)的情況把篓,這時候可以使用 useMemo 配合 memo 緩存組件。
- 避免昂貴的計算
比如下面這個例子
import React, { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
// 模擬復(fù)雜的運算嗡呼,需要兩秒鐘
const getResult = async () => {
await new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
});
return 2;
};
const onAddCount = async () => {
const result = await getResult();
setCount((count) => count + result);
};
return (
<>
<span>{count}</span>
<button onClick={onAddCount}>+隨機數(shù)</button>
</>
);
}
getResult
是一個耗時的計算纸俭,需要兩秒鐘。這就會導(dǎo)致我們每次點擊按鈕南窗,都要等待兩秒才能響應(yīng)。如果我們使用 useMemo 緩存結(jié)果,那么只有第一次需要等待兩秒万伤,后面都會快速響應(yīng)窒悔。
import React, { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
// 使用useMemo緩存復(fù)雜的計算結(jié)果
const getResult = useMemo(async () => {
await new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
});
return 2;
}, []);
const onAddCount = async () => {
// 使用useMemo直接緩存計算結(jié)果,getResult是結(jié)果不是函數(shù)
const result = await getResult;
setCount((count) => count + result);
};
return (
<>
<span>{count}</span>
<button onClick={onAddCount}>+隨機數(shù)</button>
</>
);
}
使用 useMemo 前的效果:
使用 useMemo 后的效果:
useCallback
useMemo 允許我們緩存一個函數(shù)敌买。當(dāng)再次渲染的時候简珠,返回上一次的函數(shù)而不是重新定義。
語法
const cachedFn = useCallback(fn, dependencies)
fn
: 緩存的函數(shù)虹钮。dependencies
: 依賴項聋庵。當(dāng)依賴項變化時,重新計算結(jié)果芙粱。
使用場景
- 防止組件重新渲染
當(dāng)我們傳給子組件的屬性有函數(shù)的時候,比如下面這個例子:
import React, { useState, memo, useMemo, useCallback } from 'react';
const Hello = memo(function Hello({ text, onClick }) {
console.log('子組件重新渲染');
return <h1 onClick={onClick}>{`hello ${text}!`}</h1>;
});
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const onAddCount = () => {
setCount((count) => count + 1);
};
const onChangeText = () => {
setText('world');
};
return (
<>
<span>{count}</span>
<button onClick={onAddCount}>+1</button>
<Hello text={text} onClick={onChangeText} />
</>
);
}
當(dāng)我們點擊 count 會造成子組件的渲染祭玉,這是因為 onChangeText
是引用類型,每次父組件渲染春畔,它都被重新定義脱货。這導(dǎo)致了每次 props 都發(fā)生了變化。我們可以使用 useCallback
來緩存 onChangeText
:
// 使用useCallback來緩存onChangeText
const onChangeText = useCallback(() => {
setText('world');
}, []);
使用 useMemo 也可以實現(xiàn)相同的結(jié)果律姨,只不過需要再多包一層函數(shù):
// 使用useMemo緩存onChangeText
const onChangeText = useMemo(() => {
return () => {
setText('world');
};
}, []);
- 優(yōu)化自定義 hook
如果您正在編寫自定義 Hook振峻,建議將它返回的任何函數(shù)包裝到 useCallback:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback(
(url) => {
dispatch({ type: 'navigate', url });
},
[dispatch]
);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack
};
}
這確保了 Hook 的使用者可以在需要時優(yōu)化他們自己的代碼。
爭議
有人認為應(yīng)當(dāng)給所有的函數(shù)包上 useCallback, 我并不認同择份。主要是出于以下兩個考慮:
- 使用 useCallback 后代碼可讀性變差
- 創(chuàng)建一個函數(shù)的性能消耗幾乎可以忽略不計扣孟,不應(yīng)作為優(yōu)化點
最后
官方文檔內(nèi)容較多,這里只整理個人認為比較常用的知識點荣赶。想要查漏補缺的小伙伴可以去看官網(wǎng)