React Hook之使用Effect Hook的方法
Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
這段代碼基于上一章節(jié)中的計(jì)數(shù)器示例進(jìn)行修改,我們?yōu)橛?jì)數(shù)器增加了一個(gè)小功能:將 document 的 title 設(shè)置為包含了點(diǎn)擊次數(shù)的消息。
數(shù)據(jù)獲取,設(shè)置訂閱以及手動(dòng)更改 React 組件中的 DOM 都屬于副作用。不管你知不知道這些操作,或是“副作用”這個(gè)名字,應(yīng)該都在組件中使用過它們。
提示
如果你熟悉 React class 的生命周期函數(shù),你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個(gè)函數(shù)的組合。
在 React 組件中有兩種常見副作用操作:需要清除的和不需要清除的。我們來更仔細(xì)地看一下他們之間的區(qū)別。
無需清除的 effect
有時(shí)候,我們只想在 React 更新 DOM 之后運(yùn)行一些額外的代碼。比如發(fā)送網(wǎng)絡(luò)請(qǐng)求,手動(dòng)變更 DOM,記錄日志,這些都是常見的無需清除的操作。因?yàn)槲覀冊趫?zhí)行完這些操作之后,就可以忽略他們了。讓我們對(duì)比一下使用 class 和 Hook 都是怎么實(shí)現(xiàn)這些副作用的。
使用 class 的示例
在 React 的 class 組件中,render 函數(shù)是不應(yīng)該有任何副作用的。一般來說,在這里執(zhí)行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執(zhí)行我們的操作。
這就是為什么在 React class 中,我們把副作用操作放到 componentDidMount 和 componentDidUpdate 函數(shù)中?;氐绞纠?,這是一個(gè) React 計(jì)數(shù)器的 class 組件。它在 React 對(duì) DOM 進(jìn)行操作之后,立即更新了 document 的 title 屬性
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>
);
}
}
注意,在這個(gè) class 中,我們需要在兩個(gè)生命周期函數(shù)中編寫重復(fù)的代碼。
這是因?yàn)楹芏嗲闆r下,我們希望在組件加載和更新時(shí)執(zhí)行同樣的操作。從概念上說,我們希望它在每次渲染之后執(zhí)行 —— 但 React 的 class 組件沒有提供這樣的方法。即使我們提取出一個(gè)方法,我們還是要在兩個(gè)地方調(diào)用它。
現(xiàn)在讓我們來看看如何使用 useEffect 執(zhí)行相同的操作。
使用 Hook 的示例
我們在本章節(jié)開始時(shí)已經(jīng)看到了這個(gè)示例,但讓我們再仔細(xì)觀察它:
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 做了什么?通過使用這個(gè) Hook,你可以告訴 React 組件需要在渲染后執(zhí)行某些操作。React 會(huì)保存你傳遞的函數(shù)(我們將它稱之為
effect),并且在執(zhí)行 DOM 更新之后調(diào)用它。在這個(gè)effect中,我們設(shè)置了document的title屬性,不過我們也可以執(zhí)行數(shù)據(jù)獲取或調(diào)用其他命令式的 API。 - 為什么在組件內(nèi)部調(diào)用 useEffect? 將
useEffect放在組件內(nèi)部讓我們可以在effect中直接訪問count state變量(或其他props)。我們不需要特殊的 API 來讀取它 —— 它已經(jīng)保存在函數(shù)作用域中。Hook 使用了 JavaScript 的閉包機(jī)制,而不用在 JavaScript 已經(jīng)提供了解決方案的情況下,還引入特定的 React API。 - useEffect 會(huì)在每次渲染后都執(zhí)行嗎? 是的,默認(rèn)情況下,它在第一次渲染之后和每次更新之后都會(huì)執(zhí)行。(我們稍后會(huì)談到如何控制它。)你可能會(huì)更容易接受
effect發(fā)生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運(yùn)行effect的同時(shí),DOM 都已經(jīng)更新完畢。
詳細(xì)說明
現(xiàn)在我們已經(jīng)對(duì) effect 有了大致了解,下面這些代碼應(yīng)該不難看懂了:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
我們聲明了 count state 變量,并告訴 React 我們需要使用 effect。緊接著傳遞函數(shù)給 useEffect Hook。此函數(shù)就是我們的 effect。然后使用 document.title 瀏覽器 API 設(shè)置 document 的 title。我們可以在 effect 中獲取到最新的 count 值,因?yàn)樗诤瘮?shù)的作用域內(nèi)。當(dāng) React 渲染組件時(shí),會(huì)保存已使用的 effect,并在更新完 DOM 后執(zhí)行它。這個(gè)過程在每次渲染時(shí)都會(huì)發(fā)生,包括首次渲染。
經(jīng)驗(yàn)豐富的 JavaScript 開發(fā)人員可能會(huì)注意到,傳遞給 useEffect 的函數(shù)在每次渲染中都會(huì)有所不同,這是刻意為之的。事實(shí)上這正是我們可以在 effect 中獲取最新的 count 的值,而不用擔(dān)心其過期的原因。每次我們重新渲染,都會(huì)生成新的 effect,替換掉之前的。某種意義上講,effect 更像是渲染結(jié)果的一部分 —— 每個(gè) effect “屬于”一次特定的渲染。我們將在本章節(jié)后續(xù)部分更清楚地了解這樣做的意義。
提示
與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調(diào)度的 effect 不會(huì)阻塞瀏覽器更新屏幕,這讓你的應(yīng)用看起來響應(yīng)更快。大多數(shù)情況下,effect 不需要同步地執(zhí)行。在個(gè)別情況下(例如測量布局),有單獨(dú)的 useLayoutEffect Hook 供你使用,其 API 與 useEffect 相同。
需要清除的 effect
之前,我們研究了如何使用不需要清除的副作用,還有一些副作用是需要清除的。例如訂閱外部數(shù)據(jù)源。這種情況下,清除工作是非常重要的,可以防止引起內(nèi)存泄露!現(xiàn)在讓我們來比較一下如何用 Class 和 Hook 來實(shí)現(xiàn)。
使用 Class 的示例
在 React class 中,你通常會(huì)在 componentDidMount 中設(shè)置訂閱,并在 componentWillUnmount 中清除它。例如,假設(shè)我們有一個(gè) ChatAPI 模塊,它允許我們訂閱好友的在線狀態(tài)。以下是我們?nèi)绾问褂?class 訂閱和顯示該狀態(tài):
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';
}
}
你會(huì)注意到 componentDidMount 和 componentWillUnmount 之間相互對(duì)應(yīng)。使用生命周期函數(shù)迫使我們拆分這些邏輯代碼,即使這兩部分代碼都作用于相同的副作用。
注意眼尖的讀者可能已經(jīng)注意到了,這個(gè)示例還需要編寫
componentDidUpdate方法才能保證完全正確。我們先暫時(shí)忽略這一點(diǎn),本章節(jié)中后續(xù)部分會(huì)介紹它。
使用 Hook 的示例
如何使用 Hook 編寫這個(gè)組件。
你可能認(rèn)為需要單獨(dú)的 effect 來執(zhí)行清除操作。但由于添加和刪除訂閱的代碼的緊密性,所以 useEffect 的設(shè)計(jì)是在同一個(gè)地方執(zhí)行。如果你的 effect 返回一個(gè)函數(shù),React 將會(huì)在執(zhí)行清除操作時(shí)調(diào)用它:
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);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
為什么要在 effect 中返回一個(gè)函數(shù)? 這是 effect 可選的清除機(jī)制。每個(gè) effect 都可以返回一個(gè)清除函數(shù)。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬于 effect 的一部分。
React 何時(shí)清除 effect? React 會(huì)在組件卸載的時(shí)候執(zhí)行清除操作。正如之前學(xué)到的,effect 在每次渲染的時(shí)候都會(huì)執(zhí)行。這就是為什么 React 會(huì)在執(zhí)行當(dāng)前 effect 之前對(duì)上一個(gè) effect 進(jìn)行清除。
注意
并不是必須為 effect 中返回的函數(shù)命名。這里我們將其命名為 cleanup 是為了表明此函數(shù)的目的,但其實(shí)也可以返回一個(gè)箭頭函數(shù)或者給起一個(gè)別的名字。
小結(jié)
了解了 useEffect 可以在組件渲染后實(shí)現(xiàn)各種不同的副作用。有些副作用可能需要清除,所以需要返回一個(gè)函數(shù):
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
其他的 effect 可能不必清除,所以不需要返回。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
effect Hook 使用同一個(gè) API 來滿足這兩種情況。
使用 Effect 的提示
在本節(jié)中將繼續(xù)深入了解 useEffect 的某些特性,有經(jīng)驗(yàn)的 React 使用者可能會(huì)對(duì)此感興趣。你不一定要在現(xiàn)在了解他們,你可以隨時(shí)查看此頁面以了解有關(guān) Effect Hook 的更多詳細(xì)信息。
提示:使用多個(gè) Effect 實(shí)現(xiàn)關(guān)注點(diǎn)分離
使用 Hook 其中一個(gè)目的就是要解決 class 中生命周期函數(shù)經(jīng)常包含不相關(guān)的邏輯,但又把相關(guān)邏輯分離到了幾個(gè)不同方法中的問題。下述代碼是將前述示例中的計(jì)數(shù)器和好友在線狀態(tài)指示器邏輯組合在一起的組件:
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
可以發(fā)現(xiàn)設(shè)置 document.title 的邏輯是如何被分割到 componentDidMount 和 componentDidUpdate 中的,訂閱邏輯又是如何被分割到 componentDidMount 和 componentWillUnmount 中的。而且 componentDidMount 中同時(shí)包含了兩個(gè)不同功能的代碼。
那么 Hook 如何解決這個(gè)問題呢?就像你可以使用多個(gè) state 的 Hook 一樣,你也可以使用多個(gè) effect。這會(huì)將不相關(guān)邏輯分離到不同的 effect 中:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
Hook 允許我們按照代碼的用途分離他們, 而不是像生命周期函數(shù)那樣。React 將按照 effect 聲明的順序依次調(diào)用組件中的每一個(gè) effect。
解釋:為什么每次更新的時(shí)候都要運(yùn)行 Effect
如果你已經(jīng)習(xí)慣了使用 class,那么你或許會(huì)疑惑為什么 effect 的清除階段在每次重新渲染時(shí)都會(huì)執(zhí)行,而不是只在卸載組件的時(shí)候執(zhí)行一次。讓我們看一個(gè)實(shí)際的例子,看看為什么這個(gè)設(shè)計(jì)可以幫助我們創(chuàng)建 bug 更少的組件。
在本章節(jié)開始時(shí),我們介紹了一個(gè)用于顯示好友是否在線的 FriendStatus 組件。從 class 中 props 讀取 friend.id,然后在組件掛載后訂閱好友的狀態(tài),并在卸載組件的時(shí)候取消訂閱:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是當(dāng)組件已經(jīng)顯示在屏幕上時(shí),friend prop 發(fā)生變化時(shí)會(huì)發(fā)生什么? 我們的組件將繼續(xù)展示原來的好友狀態(tài)。這是一個(gè) bug。而且我們還會(huì)因?yàn)槿∠嗛啎r(shí)使用錯(cuò)誤的好友 ID 導(dǎo)致內(nèi)存泄露或崩潰的問題。
在 class 組件中,我們需要添加 componentDidUpdate 來解決這個(gè)問題:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 取消訂閱之前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 訂閱新的 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
忘記正確地處理 componentDidUpdate 是 React 應(yīng)用中常見的 bug 來源。
現(xiàn)在看一下使用 Hook 的版本:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
它并不會(huì)受到此 bug 影響。(雖然我們沒有對(duì)它做任何改動(dòng)。)
并不需要特定的代碼來處理更新邏輯,因?yàn)?nbsp;useEffect 默認(rèn)就會(huì)處理。它會(huì)在調(diào)用一個(gè)新的 effect 之前對(duì)前一個(gè) effect 進(jìn)行清理。為了說明這一點(diǎn),下面按時(shí)間列出一個(gè)可能會(huì)產(chǎn)生的訂閱和取消訂閱操作調(diào)用序列:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 運(yùn)行第一個(gè) effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一個(gè) effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 運(yùn)行下一個(gè) effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一個(gè) effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 運(yùn)行下一個(gè) effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一個(gè) effect
此默認(rèn)行為保證了一致性,避免了在 class 組件中因?yàn)闆]有處理更新邏輯而導(dǎo)致常見的 bug。
提示:通過跳過 Effect 進(jìn)行性能優(yōu)化
在某些情況下,每次渲染后都執(zhí)行清理或者執(zhí)行 effect 可能會(huì)導(dǎo)致性能問題。在 class 組件中,我們可以通過在 componentDidUpdate 中添加對(duì) prevProps 或 prevState 的比較邏輯解決:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
這是很常見的需求,所以它被內(nèi)置到了 useEffect 的 Hook API 中。如果某些特定值在兩次重渲染之間沒有發(fā)生變化,你可以通知 React 跳過對(duì) effect 的調(diào)用,只要傳遞數(shù)組作為 useEffect 的第二個(gè)可選參數(shù)即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時(shí)更新
上面這個(gè)示例中,我們傳入 [count] 作為第二個(gè)參數(shù)。這個(gè)參數(shù)是什么作用呢?如果 count 的值是 5,而且我們的組件重渲染的時(shí)候 count 還是等于 5,React 將對(duì)前一次渲染的 [5] 和后一次渲染的 [5] 進(jìn)行比較。因?yàn)閿?shù)組中的所有元素都是相等的 (5 === 5),React 會(huì)跳過這個(gè) effect,這就實(shí)現(xiàn)了性能的優(yōu)化。
當(dāng)渲染時(shí),如果 count 的值更新成了 6,React 將會(huì)把前一次渲染時(shí)的數(shù)組 [5] 和這次渲染的數(shù)組 [6] 中的元素進(jìn)行對(duì)比。這次因?yàn)?nbsp;5 !== 6,React 就會(huì)再次調(diào)用 effect。如果數(shù)組中有多個(gè)元素,即使只有一個(gè)元素發(fā)生變化,React 也會(huì)執(zhí)行 effect。
對(duì)于有清除操作的 effect 同樣適用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 僅在 props.friend.id 發(fā)生變化時(shí),重新訂閱
未來版本,可能會(huì)在構(gòu)建時(shí)自動(dòng)添加第二個(gè)參數(shù)。
注意:
如果你要使用此優(yōu)化方式,請(qǐng)確保數(shù)組中包含了所有外部作用域中會(huì)隨時(shí)間變化并且在 effect 中使用的變量,否則你的代碼會(huì)引用到先前渲染中的舊變量。參閱文檔,了解更多關(guān)于如何處理函數(shù)以及數(shù)組頻繁變化時(shí)的措施內(nèi)容。
如果想執(zhí)行只運(yùn)行一次的 effect(僅在組件掛載和卸載時(shí)執(zhí)行),可以傳遞一個(gè)空數(shù)組([])作為第二個(gè)參數(shù)。這就告訴 React 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠(yuǎn)都不需要重復(fù)執(zhí)行。這并不屬于特殊情況 —— 它依然遵循依賴數(shù)組的工作方式。
如果你傳入了一個(gè)空數(shù)組([]),effect 內(nèi)部的 props 和 state 就會(huì)一直擁有其初始值。盡管傳入 [] 作為第二個(gè)參數(shù)更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思維模式,但我們有更好的方式來避免過于頻繁的重復(fù)調(diào)用 effect。除此之外,請(qǐng)記得 React 會(huì)等待瀏覽器完成畫面渲染之后才會(huì)延遲調(diào)用 useEffect,因此會(huì)使得額外操作很方便。
我們推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規(guī)則。此規(guī)則會(huì)在添加錯(cuò)誤依賴時(shí)發(fā)出警告并給出修復(fù)建議。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
React Router 5.1.0使用useHistory做頁面跳轉(zhuǎn)導(dǎo)航的實(shí)現(xiàn)
本文主要介紹了React Router 5.1.0使用useHistory做頁面跳轉(zhuǎn)導(dǎo)航的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
詳解React中的setState執(zhí)行機(jī)制
setState是React組件中用于更新狀態(tài)的方法,是類組件中的方法,用于更新組件的狀態(tài)并重新渲染組件,本文給大家詳細(xì)介紹了React中的setState執(zhí)行機(jī)制,文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
react使用axios進(jìn)行api網(wǎng)絡(luò)請(qǐng)求的封裝方法詳解
這篇文章主要為大家詳細(xì)介紹了react使用axios進(jìn)行api網(wǎng)絡(luò)請(qǐng)求的封裝方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
詳解react-router如何實(shí)現(xiàn)按需加載
本篇文章主要介紹了react-router如何實(shí)現(xiàn)按需加載,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06
react源碼中的生命周期和事件系統(tǒng)實(shí)例解析
這篇文章主要為大家介紹了react源碼中的生命周期和事件系統(tǒng)實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01

