React Hook是React函數(shù)式組件,它不僅僅有函數(shù)組件的特性,還帶有React框架的特性。所以,官網(wǎng)文檔多次強(qiáng)調(diào):
只在 React 函數(shù)中調(diào)用 Hook
不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook。你可以:
- ? 在 React 的函數(shù)組件中調(diào)用 Hook
- ? 在自定義 Hook 中調(diào)用其他 Hook
1. 那么 React 中 Function Component 與 Class Component 有何不同?
Class Component:
class ProfilePage extends React.Component {
showMessage = () => {
alert("Followed " + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
Function Component:
function ProfilePage(props) {
const showMessage = () => {
alert("Followed " + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
如下父級(jí)組件的調(diào)用方式:
<ProfilePageFunction user={this.state.user} />
<ProfilePageClass user={this.state.user} />
場(chǎng)景:那么當(dāng)點(diǎn)擊按鈕后的 3 秒內(nèi),父級(jí)修改了 this.state.user,彈出的用戶(hù)名是修改前的還是修改后的呢?
答案:Class Component 展示的是修改后的值,F(xiàn)unction Component 展示的是修改前的值
原因:this 在 Class Component 中是可變的,當(dāng)組件入?yún)l(fā)生變化時(shí), this.props同步改變。而 Function Component 不存在this.props 的語(yǔ)法,因此 props 總是不可變的。
2. 用Hook 創(chuàng)建函數(shù)組件,會(huì)有什么變化呢?
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
function Example() {
const [count, setCount] = useState(0);
const handleAlertClick = useCallback(()=>{
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000)
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
增加 count
</button>
<button onClick={handleAlertClick}>
顯示 count
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
場(chǎng)景:點(diǎn)擊 顯示 按鈕,在 3s 后(模擬耗時(shí)任務(wù))會(huì)出現(xiàn)彈層。在這 3s 期間快速點(diǎn)擊 增加 count 按鈕
結(jié)果:3s 后看到的彈層計(jì)數(shù)仍舊為 0!
原因:在 handleAlertClick 函數(shù)執(zhí)行的那個(gè) Render 過(guò)程里,count 的值可以看作常量 。執(zhí)行 setCount(count + 1) 時(shí)會(huì)交由一個(gè)全新的 Render 渲染,所以不會(huì)執(zhí)行 handleAlertClick 函數(shù)。
那我們用useEffect 改造一下呢?
const [count, setCount] = useState(0);
useEffect(()=>{
setTimeout(() => {
alert('count: ' + count);
}, 3000)
}, [count]); // 監(jiān)聽(tīng)count變化
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
增加 count
</button>
</div>
);
}
...
行為:三秒鐘內(nèi)點(diǎn)擊“增加 count”按鈕三次。
結(jié)果:先顯示0(初始化),然依次顯示1,2,3,useEffect被觸發(fā)4次。
原因:useEffect也是具有 Capture Value 的特性,每次render都是獨(dú)立快照,拿到的 count 都是固化下來(lái)的常量。和上面例子不同之處在于,它監(jiān)聽(tīng)了count變化,可以被觸發(fā)多次Render。
什么是Capture Value?
每次 Render 的內(nèi)容都會(huì)形成一個(gè)快照并保留下來(lái),因此當(dāng)狀態(tài)變更而 Rerender 時(shí),就形成了 N 個(gè) Render 狀態(tài),而每個(gè) Render 狀態(tài)都擁有自己固定不變的 Props 與 State。
3. 如何繞過(guò) Capture Value?
利用 useRef !
useRef定義
useRef 返回一個(gè)可變的 ref 對(duì)象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對(duì)象在組件的整個(gè)生命周期內(nèi)保持不變。
ref 類(lèi)型的變量通常是用來(lái)存儲(chǔ) DOM 元素引用。
但在 react hooks 中,它可以存放任何可變數(shù)據(jù),并在所有 Render 過(guò)程中保持著唯一引用,因此所有對(duì) ref 的賦值或取值,拿到的都只有一個(gè)最終狀態(tài),而不會(huì)在每個(gè) Render 間存在隔離。
function Example() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
const handleAlertClick = useCallback(
() => {
setTimeout(() => {
alert("You clicked on: " + countRef.current);
}, 3000);
},
[count]
);
return (
<div>
<p>You clicked {count} times</p>
<button
onClick={() => {
countRef.current = count + 1;
setCount(count + 1);
}}
>
增加 count
</button>
<button onClick={handleAlertClick}>顯示 count</button>
</div>
);
行為:三秒鐘內(nèi)點(diǎn)擊“增加 count”按鈕三次。
結(jié)果: 先顯示0(初始化),3s 后顯示3次3。

4. useEffect搭配useReducer
利用 useEffect 的兄弟 useReducer 函數(shù),可以將更新與動(dòng)作解耦。
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
這就是一個(gè)局部 “Redux”,由于更新變成了dispatch({ type: "tick" }) 所以不管更新時(shí)需要依賴(lài)多少變量,在調(diào)用更新的動(dòng)作里都不需要依賴(lài)任何變量。 具體更新操作在 reducer 函數(shù)里寫(xiě)就可以了
5. useMemo vs useCallback
按照官網(wǎng)說(shuō)法,兩個(gè)的相同點(diǎn)和不同點(diǎn)非常明確(React教程)
- 相同點(diǎn):
- 都會(huì)在組件第一次渲染的時(shí)候執(zhí)行
- 依賴(lài)的變量發(fā)生改變時(shí)再次執(zhí)行
- 返回緩存的值
- 不同點(diǎn):
-
useMemo返回緩存的變量 -
useCallback返回緩存的函數(shù)
-
useCallback(fn, deps)相當(dāng)于useMemo(() => fn, deps)
5.1 useMemo實(shí)例
export function useDocumentMap() {
const { docState: { sideBarList = [] } = {} } = useStore()
return useMemo(() => getDocumentMap(sideBarList), [sideBarList])
}
getDocumentMap是一個(gè)復(fù)雜計(jì)算函數(shù),sideBarList為store里存儲(chǔ)的值。UI顯示的時(shí)候,需要做一次model=>UI model的轉(zhuǎn)換。因?yàn)檫@個(gè)轉(zhuǎn)換需要復(fù)雜計(jì)算,所以用useMemo做緩存,在sideBarList不變的情況下,調(diào)用useDocumentMap獲取UI model只會(huì)計(jì)算一次。
5.2 useCallback實(shí)例
import React, { useState, useCallback } from 'react';
import Button from './Button';
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<div>
<div>
<Button onClickButton={handleClickButton1}>Button1</Button>
</div>
<div>
<Button onClickButton={handleClickButton2}>Button2</Button>
</div>
</div>
);
}
// Button組件
import React from 'react';
const Button = ({ onClickButton, children }) => {
return (
<>
<button onClick={onClickButton}>{children}</button>
<span>{Math.random()}</span>
</>
);
};
export default React.memo(Button);
可見(jiàn)Button組件只依賴(lài)onClickButton和children。
如果點(diǎn)擊Button1,因?yàn)?code>count1的值發(fā)生變化,導(dǎo)致容器App重新渲染,handleClickButton1變量重新被聲明(又創(chuàng)建了一個(gè)新的函數(shù)對(duì)象),從而第一個(gè)Button被再次渲染。而handleClickButton2呢?因?yàn)楸?code>useCallback包了一層,且依賴(lài)count2,所以,此時(shí)handleClickButton2變量不會(huì)被重新聲明,還是之前的函數(shù)引用地址,所以,第二個(gè)Button不會(huì)被重復(fù)渲染。
React.memo這個(gè)方法,會(huì)對(duì) props 做一個(gè)淺層比較,如果 props 沒(méi)有發(fā)生改變,則不會(huì)重新渲染此組件
5.3 總結(jié)
- 如果有函數(shù)傳遞給子組件,使用
useCallback - 如果有值傳遞給子組件,使用
useMemo -
useMemo、useCallback都自帶閉包,類(lèi)似useEffect的Capture Value能力