Hooks Api 索引
基礎(chǔ) Api
useState |
useEffect |
useContext |
|---|---|---|
| 返回一個(gè) state,以及更新 state 的函數(shù)。 | 相當(dāng)于componentDidMount、componentDidUpdate,componentWillUnmount 這三個(gè)函數(shù)的組合。 | 接收一個(gè) context 對(duì)象(React.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <MyContext.Provider> 的 value prop 決定。 |
額外Api
useReducer |
useCallback |
useMemo |
useRef |
useImperativeHandle |
useLayoutEffect |
useDebugValue |
|---|
useState 官方文檔例子
import React, { useState } from 'react';
function Example() {
// 聲明一個(gè)叫 "count" 的 state 變量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
等價(jià)的class組件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
聲明多個(gè)state
function ExampleWithManyStates() {
// 聲明多個(gè) state 變量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '學(xué)習(xí) Hook' }]);
提示:方括號(hào)有什么用?
我們用方括號(hào)定義了一個(gè) state 變量
等號(hào)左邊名字并不是 React API 的部分,你可以自己取名字
這種 JavaScript 語(yǔ)法叫數(shù)組解構(gòu)。它意味著我們同時(shí)創(chuàng)建了fruit和setFruit兩個(gè)變量,fruit的值為useState返回的第一個(gè)值,setFruit是返回的第二個(gè)值
const [fruit, setFruit] = useState('banana');
var fruitStateVariable = useState('banana'); // 返回一個(gè)有兩個(gè)元素的數(shù)組
var fruit = fruitStateVariable[0]; // 數(shù)組里的第一個(gè)值
var setFruit = fruitStateVariable[1]; // 數(shù)組里的第二個(gè)值
useState除了可以靜態(tài)的賦值還可以u(píng)seState(()=>init)
useState返回的更新數(shù)據(jù)方法setXXX(currentState=>currentState+1)默認(rèn)會(huì)傳入當(dāng)前的數(shù)據(jù)狀態(tài)
useEffect
Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作
解釋這個(gè) Hook 之前先理解下什么是副作用。網(wǎng)絡(luò)請(qǐng)求、訂閱某個(gè)模塊或者 DOM 操作都是副作用的例子,Effect Hook 是專(zhuān)門(mén)用來(lái)處理副作用的。正常情況下,在Function Component的函數(shù)體中,是不建議寫(xiě)副作用代碼的,否則容易出 bug。
下面的Class Component例子中,副作用代碼寫(xiě)在了componentDidMount和componentDidUpdate中:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
可以看到componentDidMount和componentDidUpdate中的代碼是一樣的。而使用 Effect Hook 來(lái)改寫(xiě)就不會(huì)有這個(gè)問(wèn)題:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
//useEffect 沒(méi)有傳遞依賴參數(shù),所以每次數(shù)據(jù)更新都會(huì)調(diào)用useEffect
useEffect會(huì)在每次 DOM 渲染后執(zhí)行,不會(huì)阻塞頁(yè)面渲染。它同時(shí)具備componentDidMount、componentDidUpdate和componentWillUnmount三個(gè)生命周期函數(shù)的執(zhí)行時(shí)機(jī)
此外還有一些副作用需要組件卸載的時(shí)候做一些額外的清理工作的,例如訂閱某個(gè)功能:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
在componentDidMount訂閱后,需要在componentWillUnmount取消訂閱。使用 Effect Hook 來(lái)改寫(xiě)會(huì)是這個(gè)樣子:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 返回一個(gè)函數(shù)來(lái)進(jìn)行額外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
當(dāng)useEffect的返回值是一個(gè)函數(shù)的時(shí)候,React 會(huì)在下一次執(zhí)行這個(gè)副作用之前執(zhí)行一遍清理工作,整個(gè)組件的生命周期流程可以這么理解:
組件掛載 --> 執(zhí)行副作用 --> 組件更新 --> 執(zhí)行清理函數(shù) --> 執(zhí)行副作用 --> 組件更新 --> 執(zhí)行清理函數(shù) --> 組件卸載
上文提到useEffect會(huì)在每次渲染后執(zhí)行,但有的情況下我們希望只有在 state 或 props 改變的情況下才執(zhí)行。如果是Class Component,我們會(huì)這么做:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
使用 Hook 的時(shí)候,我們只需要傳入第二個(gè)參數(shù):
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改變的時(shí)候才執(zhí)行 Effect
第二個(gè)參數(shù)是一個(gè)數(shù)組,可以傳多個(gè)值,一般會(huì)將 Effect 用到的所有 props 和 state 都傳進(jìn)去。
當(dāng)副作用只需要在組件掛載的時(shí)候和卸載的時(shí)候執(zhí)行,第二個(gè)參數(shù)可以傳一個(gè)空數(shù)組[],實(shí)現(xiàn)的效果有點(diǎn)類(lèi)似componentDidMount和componentWillUnmount的組合。
useCallback useMemo
在介紹一下這兩個(gè)hooks的作用之前,我們先來(lái)回顧一下react中的性能優(yōu)化。在hooks誕生之前,如果組件包含內(nèi)部state,我們都是基于class的形式來(lái)創(chuàng)建組件。當(dāng)時(shí)我們也知道,react中,性能的優(yōu)化點(diǎn)在于:
1.調(diào)用setState,就會(huì)觸發(fā)組件的重新渲染,無(wú)論前后的state是否不同
2.父組件更新,子組件也會(huì)自動(dòng)的更新
基于上面的兩點(diǎn),我們通常的解決方案是:使用immutable進(jìn)行比較,在不相等的時(shí)候調(diào)用setState;在shouldComponentUpdate中判斷前后的props和state,如果沒(méi)有變化,則返回false來(lái)阻止更新。
在hooks出來(lái)之后,我們能夠使用function的形式來(lái)創(chuàng)建包含內(nèi)部state的組件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我們無(wú)法通過(guò)判斷前后狀態(tài)來(lái)決定是否更新。而且,在函數(shù)組件中,react不再區(qū)分mount和update兩個(gè)狀態(tài),這意味著函數(shù)組件的每一次調(diào)用都會(huì)執(zhí)行其內(nèi)部的所有邏輯,那么會(huì)帶來(lái)較大的性能損耗。因此useMemo 和useCallback就是解決性能問(wèn)題的殺手锏
useCallback和useMemo的參數(shù)跟useEffect一致,他們之間最大的區(qū)別有是useEffect會(huì)用于處理副作用,而前兩個(gè)hooks不能。
useMemo和useCallback都會(huì)在組件第一次渲染的時(shí)候執(zhí)行,之后會(huì)在其依賴的變量發(fā)生改變時(shí)再次執(zhí)行;并且這兩個(gè)hooks都返回緩存的值,useMemo返回緩存的變量,useCallback返回緩存的函數(shù)。
useMemo
我們來(lái)看一個(gè)反例:
import React from 'react';
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
這里創(chuàng)建了兩個(gè)state,執(zhí)行一次昂貴的計(jì)算,拿到count對(duì)應(yīng)的某個(gè)值。我們可以看到:無(wú)論是修改count還是val,由于組件的重新渲染,都會(huì)觸發(fā)expensive的執(zhí)行(能夠在控制臺(tái)看到,即使修改val,也會(huì)打印);但是這里的昂貴計(jì)算只依賴于count的值,在val修改的時(shí)候,是沒(méi)有必要再次計(jì)算的。在這種情況下,我們就可以使用useMemo,只在count的值修改時(shí),執(zhí)行expensive計(jì)算:
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>;
}
上面我們可以看到,使用useMemo來(lái)執(zhí)行昂貴的計(jì)算,然后將計(jì)算值返回,并且將count作為依賴值傳遞進(jìn)去。這樣,就只會(huì)在count改變的時(shí)候觸發(fā)expensive執(zhí)行,在修改val的時(shí)候,返回上一次緩存的值。
useCallback
講完了useMemo,接下來(lái)是useCallback。useCallback跟useMemo比較類(lèi)似,但它返回的是緩存的函數(shù)。我們看一下最簡(jiǎn)單的用法
const fnA = useCallback(fnB, [a])
上面的useCallback會(huì)將我們傳遞給它的函數(shù)fnB返回,并且將這個(gè)結(jié)果緩存;當(dāng)依賴a變更時(shí),會(huì)返回新的函數(shù)。既然返回的是函數(shù),我們無(wú)法很好的判斷返回的函數(shù)是否變更,所以我們可以借助ES6新增的數(shù)據(jù)類(lèi)型Set來(lái)判斷,具體如下:
import React, { useState, useCallback } from 'react';
const set = new Set();
export default function Callback() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log(count);
}, [count]);
set.add(callback);
return <div>
<h4>{count}</h4>
<h4>{set.size}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
我們可以看到,每次修改count,set.size就會(huì)+1,這說(shuō)明useCallback依賴變量count,count變更時(shí)會(huì)返回新的函數(shù);而val變更時(shí),set.size不會(huì)變,說(shuō)明返回的是緩存的舊版本函數(shù)。
知道useCallback有什么樣的特點(diǎn),那有什么作用呢?
使用場(chǎng)景是:有一個(gè)父組件,其中包含子組件,子組件接收一個(gè)函數(shù)作為props;通常而言,如果父組件更新了,子組件也會(huì)執(zhí)行更新;但是大多數(shù)場(chǎng)景下,更新是沒(méi)有必要的,我們可以借助useCallback來(lái)返回函數(shù),然后把這個(gè)函數(shù)作為props傳遞給子組件;這樣,子組件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
useEffect、useMemo、useCallback都是自帶閉包的。也就是說(shuō),每一次組件的渲染,其都會(huì)捕獲當(dāng)前組件函數(shù)上下文中的狀態(tài)(state, props),所以每一次這三種hooks的執(zhí)行,反映的也都是當(dāng)前的狀態(tài),你無(wú)法使用它們來(lái)捕獲上一次的狀態(tài)。對(duì)于這種情況,我們應(yīng)該使用ref來(lái)訪問(wèn)。
有空繼續(xù)整理