Effect Hook
可以使得你在函數組件中執行一些帶有副作用的方法。
import { 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>
);
}
上面這段代碼是基于上個state hook計數器的例子,但是現在添加了新的功能: 我們將文檔標題設置為自定義消息,包括點擊次數。
數據獲取,設置訂閱以及手動更改React
組件中的DOM
都是副作用的示例。無論你是否習慣于將這些操作稱為“副作用”(或僅僅是“效果”),但你之前可能已經在組件中執行了這些操作。
提示: 如果你熟悉
React
類生命周期方法,則可以將useEffect Hook
視為componentDidMount
,componentDidUpdate
和componentWillUnmount
的組合。
React組件中有兩種常見的副作用:那些不需要清理的副作用,以及那些需要清理的副作用。讓我們更詳細地看一下這種區別。
無需清理的副作用
有時,我們希望在React
更新DOM
之后運行一些額外的代碼。 網絡請求,手動改變DOM
和日志記錄是不需要清理的效果(副作用,簡稱'效果')的常見示例。我們這樣說是因為我們可以運行它們并立即忘記它們。讓我們比較一下class
和hooks
如何讓我們表達這樣的副作用。
使用class的例子
在React
類組件中,render
方法本身不應該導致副作用。這太早了 - 我們通常希望在React
更新DOM
之后執行我們的效果。
這就是為什么在React
類中,我們將副作用放入componentDidMount
和componentDidUpdate
中。回到我們的示例,這里是一個React
計數器類組件,它在React
對DOM
進行更改后立即更新文檔標題:
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>
);
}
}
請注意我們如何在類中復制這兩個生命周期方法之間的代碼。
這是因為在許多情況下,我們希望執行相同的副作用,無論組件是剛安裝還是已更新。從概念上講,我們希望它在每次渲染之后發生 - 但是React類組件沒有這樣的方法(render方法應該避免副作用)。我們可以提取一個單獨的方法,但我們仍然需要在兩個地方調用它。
現在讓我們看看我們如何使用useEffect Hook
做同樣的事情。
使用Hooks的例子
import { 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
有什么作用? 通過使用這個Hook
,你告訴React
你的組件需要在渲染后執行某些操作。React
將記住你傳遞的函數(我們將其稱為“效果”),并在執行DOM
更新后稍后調用它。在這個效果中,我們設置文檔標題,但我們也可以執行數據提取或調用其他命令式API
。
為什么在組件內調用useEffect
? 在組件中使用useEffect
讓我們可以直接從效果中訪問狀態變量(如count
或任何道具)。我們不需要特殊的API
來讀取它 - 它已經在函數范圍內了。Hooks
擁抱JavaScript
閉包,并避免在JavaScript
已經提供解決方案的情況下引入特定于React
的API
。
每次渲染后useEffect都會運行嗎? 是的。默認情況下,它在第一次渲染之后和每次更新之后運行。 (我們稍后會討論如何自定義它。)你可能會發現更容易認為效果發生在“渲染之后”,而不是考慮“掛載”和“更新”。React
保證DOM
在運行‘效果’時已更新。
詳細說明
現在我們對這個hook
更加的了解了,那讓我們再看看下面的例子:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
我們聲明了count
狀態變量,然后告訴React
我們需要使用效果。我們將一個函數傳遞給useEffect Hook
,這個函數就是效果(副作用)。在我們的效果中,我們使用document.title
瀏覽器API
設置文檔標題。我們可以讀取效果中的最新count
,因為它在我們的函數范圍內。當React
渲染我們的組件時,它會記住我們使用的效果,然后在更新DOM
后運行我們的效果。每次渲染都會發生這種情況,包括第一次渲染。
有經驗的JavaScript
開發人員可能會注意到,傳遞給useEffect
的函數在每次渲染時都會有所不同。這是有意的。事實上,這就是讓我們從效果中讀取計數值而不用擔心它沒有改變的原因。每次我們重新渲染時,我們都會安排一個不同的效果,取代之前的效果。在某種程度上,這使得效果更像是渲染結果的一部分 - 每個效果“屬于”特定渲染。我們將在本頁后面更清楚地看到為什么這有用。
注意: 與
componentDidMount
或componentDidUpdate
不同,使用useEffect
的效果不會阻止瀏覽器更新屏幕。這使應用感覺更具響應性。大多數效果不需要同步發生。在他們這樣做的不常見情況下(例如測量布局),有一個單獨的useLayoutEffect Hook,其API
與useEffect
相同。
需要清理的副作用
之前,我們研究了如何表達不需要任何清理的副作用。但是,有些效果需要清理。例如,我們可能希望設置對某些外部數據源的訂閱。在這種情況下,清理是非常重要的,這樣我們就不會引入內存泄漏!讓我們比較一下我們如何使用類和Hooks
來實現它。
使用class
的例子
在React
類中,通常會在componentDidMount
中設置訂閱,并在componentWillUnmount
中清除它。例如,假設我們有一個ChatAPI
模塊,可以讓我們訂閱朋友的在線狀態。以下是我們如何使用類訂閱和顯示該狀態:
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
如何相互作用。生命周期方法迫使我們拆分這個邏輯,即使它們中的概念代碼都與相同的效果有關。
注意: 眼尖的你可能會注意到這個例子還需要一個
componentDidUpdate
方法才能完全正確。我們暫時忽略這一點,但會在本頁的后面部分再回過頭來討論它。
使用hooks
的例子
你可能認為我們需要單獨的效果來執行清理。但是添加和刪除訂閱的代碼是如此緊密相關,以至于useEffect
旨在將它保持在一起。如果你的效果返回一個函數,React將在清理時運行它:
import { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
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';
}
為什么我們從效果中返回一個函數? 這是效果的可選清理機制。每個效果都可能返回一個在它之后清理的函數。這使我們可以保持添加和刪除彼此接近的訂閱的邏輯。
React什么時候清理效果? 當組件卸載時,React
執行清理。但是,正如我們之前所了解的那樣,效果會針對每個渲染運行而不僅僅是一次。這就是React
在下次運行效果之前還清除前一渲染效果的原因。我們將討論為什么這有助于避免錯誤以及如何在以后發生性能問題時選擇退出此行為。
注意 我們不必從效果中返回命名函數。我們在這里只是為了說明才加的命名,但你可以返回箭頭函數。
概括
我們已經了解到useEffect
讓我們在組件渲染后表達不同類型的副作用。某些效果可能需要清理,因此它們返回一個函數:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
其他效果可能沒有清理階段,也不會返回任何內容。比如:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
如果你覺得你對Effect Hook
的工作方式有了很好的把握,或者你感到不知所措,那么現在就可以跳轉到關于Hooks
規則。
使用效果的提示
我們將繼續深入了解使用React
用戶可能會產生好奇心的useEffect
的某些方面。
提示:使用多重效果分離問題
這是一個組合了前面示例中的計數器和朋友狀態指示器邏輯的組件:
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
});
}
// ...
請注意設置document.title
的邏輯如何在componentDidMount
和componentDidUpdate
之間拆分。訂閱邏輯也在componentDidMount
和componentWillUnmount
之間傳播。componentDidMount
包含兩個任務的代碼。
那么,Hooks
如何解決這個問題呢?就像你可以多次使用狀態掛鉤一樣,你也可以使用多種效果。這讓我們將不相關的邏輯分成不同的效果:
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
}
Hooks
允許我們根據它正在做的事情而不是生命周期方法名稱來拆分代碼。 React
將按照指定的順序應用組件使用的每個效果。
說明:為什么效果在每個更新上運行
如果你習慣了類,你可能想知道為什么每次重新渲染后效果的清理階段都會發生,而不是在卸載過程中只發生一次。讓我們看一個實際的例子,看看為什么這個設計可以幫助我們創建更少bug的組件。
在上面介紹了一個示例FriendStatus
組件,該組件顯示朋友是否在線。我們的類從this.props
讀取friend.id
,在組件掛載后訂閱朋友狀態,并在卸載期間取消訂閱:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是如果friend prop
在組件出現在屏幕上時發生了變化,會發生什么? 我們的組件將繼續顯示不同朋友的在線狀態。這是一個錯誤。卸載時我們還會導致內存泄漏或崩潰,因為取消訂閱會使用錯誤的朋友ID。
在類組件中,我們需要添加componentDidUpdate
來處理這種情況:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
忘記正確處理componentDidUpdate
是React
應用程序中常見的bug
漏洞。
現在考慮使用Hooks的這個組件的版本:
function FriendStatus(props) {
// ...
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
它不會受到這個bug
的影響。 (但我們也沒有對它做任何改動。)
沒有用于處理更新的特殊代碼,因為默認情況下useEffect會處理它們。它會在應用下一個效果之前清除之前的效果。為了說明這一點,這里是一個訂閱和取消訂閱調用的序列,該組件可以隨著時間的推移產生:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
此行為默認確保一致性,并防止由于缺少更新邏輯而導致類組件中常見的錯誤。
提示:通過跳過效果優化性能
在某些情況下,在每次渲染后清理或應用效果可能會產生性能問題。在類組件中,我們可以通過在componentDidUpdate
中編寫與prevProps
或prevState
的額外比較來解決這個問題:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
這個要求很常見,它被內置到useEffect Hook API
中。如果在重新渲染之間沒有更改某些值,則可以告訴React
跳過應用效果。為此,將數組作為可選的第二個參數傳遞給useEffect
:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 當count改變的時候回再次運行這個效果
在上面的例子中,我們傳遞[count]
作為第二個參數。這是什么意思?如果count
為5,然后我們的組件重新渲染,count
仍然等于5,則React
將比較前一個渲染的[5]和下一個渲染的[5]。因為數組中的所有項都是相同的(5 === 5
),所以React
會跳過這個效果。這是我們的優化。
當我們使用count
更新為6渲染時,React
會將前一渲染中[5]數組中的項目與下一渲染中[6]數組中的項目進行比較。這次,React
將重新運行效果,因為5!== 6
。如果數組中有多個項目,React
將重新運行效果,即使其中只有一個不同。
這也適用于具有清理階段的效果:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
將來, 第二個參數可能會通過構建時轉換自動添加。
注意 如果使用此優化,請確保該數組包含外部作用域中隨時間變化且效果使用的任何值,換句話說就是要在這個效果函數里有意義。否則,代碼將引用先前渲染中的舊值。我們還將討論
Hooks API
參考中的其他優化選項。如果要運行效果并僅將其清理一次(在裝載和卸載時),則可以將空數組([])作為第二個參數傳遞。 這告訴
React
你的效果不依賴于來自props
或state
的任何值,所以它永遠不需要重新運行。這不作為特殊情況處理 - 它直接遵循輸入數組的工作方式。雖然傳遞[]更接近熟悉的componentDidMount和componentWillUnmount心理模型,但我們建議不要將它作為一種習慣,因為它經常會導致錯誤,如上所述。 不要忘記React
推遲運行useEffect
直到瀏覽器繪制完成后,所以做額外的工作不是問題。
更多的關于hook系列介紹, 請前往此處查看