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