<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Effect Hook 可以讓你在函陣列件中執行副作用操作
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> ); }
這段程式碼基於上一章節中的計數器範例進行修改,我們為計數器增加了一個小功能:將 document
的 title
設定為包含了點選次數的訊息。
資料獲取,設定訂閱以及手動更改 React 元件中的 DOM 都屬於副作用。不管你知不知道這些操作,或是“副作用”這個名字,應該都在元件中使用過它們。
提示
如果你熟悉 React class
的生命週期函數,你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
這三個函數的組合。
在 React 元件中有兩種常見副作用操作:需要清除的和不需要清除的。我們來更仔細地看一下他們之間的區別。
有時候,我們只想在 React 更新 DOM 之後執行一些額外的程式碼。比如傳送網路請求,手動變更 DOM,記錄紀錄檔,這些都是常見的無需清除的操作。因為我們在執行完這些操作之後,就可以忽略他們了。讓我們對比一下使用 class
和 Hook 都是怎麼實現這些副作用的。
在 React 的 class
元件中,render
函數是不應該有任何副作用的。一般來說,在這裡執行操作太早了,我們基本上都希望在 React 更新 DOM 之後才執行我們的操作。
這就是為什麼在 React class 中,我們把副作用操作放到 componentDidMount
和 componentDidUpdate
函數中。回到範例中,這是一個 React 計數器的 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 中,我們需要在兩個生命週期函數中編寫重複的程式碼。
這是因為很多情況下,我們希望在元件載入和更新時執行同樣的操作。從概念上說,我們希望它在每次渲染之後執行 —— 但 React 的 class
元件沒有提供這樣的方法。即使我們提取出一個方法,我們還是要在兩個地方呼叫它。
現在讓我們來看看如何使用 useEffect
執行相同的操作。
我們在本章節開始時已經看到了這個範例,但讓我們再仔細觀察它:
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> ); }
effect
),並且在執行 DOM 更新之後呼叫它。在這個 effect
中,我們設定了 document
的 title
屬性,不過我們也可以執行資料獲取或呼叫其他命令式的 API。useEffect
放在元件內部讓我們可以在 effect
中直接存取 count state
變數(或其他 props
)。我們不需要特殊的 API 來讀取它 —— 它已經儲存在函數作用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供瞭解決方案的情況下,還引入特定的 React API。effect
發生在“渲染之後”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次執行 effect
的同時,DOM 都已經更新完畢。現在我們已經對 effect
有了大致瞭解,下面這些程式碼應該不難看懂了:
function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); }
我們宣告了 count state
變數,並告訴 React 我們需要使用 effect
。緊接著傳遞函數給 useEffect
Hook。此函數就是我們的 effect
。然後使用 document.title
瀏覽器 API 設定 document
的 title
。我們可以在 effect
中獲取到最新的 count
值,因為他在函數的作用域內。當 React 渲染元件時,會儲存已使用的 effect
,並在更新完 DOM 後執行它。這個過程在每次渲染時都會發生,包括首次渲染。
經驗豐富的 JavaScript 開發人員可能會注意到,傳遞給 useEffect
的函數在每次渲染中都會有所不同,這是刻意為之的。事實上這正是我們可以在 effect
中獲取最新的 count
的值,而不用擔心其過期的原因。每次我們重新渲染,都會生成新的 effect
,替換掉之前的。某種意義上講,effect
更像是渲染結果的一部分 —— 每個 effect
“屬於”一次特定的渲染。我們將在本章節後續部分更清楚地瞭解這樣做的意義。
提示
與 componentDidMount
或 componentDidUpdate
不同,使用 useEffect
排程的 effect
不會阻塞瀏覽器更新螢幕,這讓你的應用看起來響應更快。大多數情況下,effect
不需要同步地執行。在個別情況下(例如測量佈局),有單獨的 useLayoutEffect
Hook 供你使用,其 API 與 useEffect
相同。
之前,我們研究瞭如何使用不需要清除的副作用,還有一些副作用是需要清除的。例如訂閱外部資料來源。這種情況下,清除工作是非常重要的,可以防止引起記憶體洩露!現在讓我們來比較一下如何用 Class 和 Hook 來實現。
在 React class 中,你通常會在 componentDidMount
中設定訂閱,並在 componentWillUnmount
中清除它。例如,假設我們有一個 ChatAPI
模組,它允許我們訂閱好友的線上狀態。以下是我們如何使用 class 訂閱和顯示該狀態:
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
方法才能保證完全正確。我們先暫時忽略這一點,本章節中後續部分會介紹它。
如何使用 Hook 編寫這個元件。
你可能認為需要單獨的 effect
來執行清除操作。但由於新增和刪除訂閱的程式碼的緊密性,所以 useEffect
的設計是在同一個地方執行。如果你的 effect
返回一個函數,React 將會在執行清除操作時呼叫它:
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 中返回一個函數? 這是 effect
可選的清除機制。每個 effect
都可以返回一個清除函數。如此可以將新增和移除訂閱的邏輯放在一起。它們都屬於 effect
的一部分。
React 何時清除 effect? React 會在元件解除安裝的時候執行清除操作。正如之前學到的,effect
在每次渲染的時候都會執行。這就是為什麼 React 會在執行當前 effect
之前對上一個 effect
進行清除。
注意
並不是必須為 effect
中返回的函數命名。這裡我們將其命名為 cleanup
是為了表明此函數的目的,但其實也可以返回一個箭頭函數或者給起一個別的名字。
瞭解了 useEffect
可以在元件渲染後實現各種不同的副作用。有些副作用可能需要清除,所以需要返回一個函數:
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 來滿足這兩種情況。
在本節中將繼續深入瞭解 useEffect
的某些特性,有經驗的 React 使用者可能會對此感興趣。你不一定要在現在瞭解他們,你可以隨時檢視此頁面以瞭解有關 Effect Hook 的更多詳細資訊。
使用 Hook 其中一個目的就是要解決 class
中生命週期函數經常包含不相關的邏輯,但又把相關邏輯分離到了幾個不同方法中的問題。下述程式碼是將前述範例中的計數器和好友線上狀態指示器邏輯組合在一起的元件:
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
中同時包含了兩個不同功能的程式碼。
那麼 Hook 如何解決這個問題呢?就像你可以使用多個 state
的 Hook 一樣,你也可以使用多個 effect
。這會將不相關邏輯分離到不同的 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 允許我們按照程式碼的用途分離他們, 而不是像生命週期函數那樣。React 將按照 effect
宣告的順序依次呼叫元件中的每一個 effect
。
如果你已經習慣了使用 class
,那麼你或許會疑惑為什麼 effect
的清除階段在每次重新渲染時都會執行,而不是隻在解除安裝元件的時候執行一次。讓我們看一個實際的例子,看看為什麼這個設計可以幫助我們建立 bug 更少的元件。
在本章節開始時,我們介紹了一個用於顯示好友是否線上的 FriendStatus
元件。從 class
中 props
讀取 friend.id
,然後在元件掛載後訂閱好友的狀態,並在解除安裝元件的時候取消訂閱:
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }
但是當元件已經顯示在螢幕上時,friend prop 發生變化時會發生什麼? 我們的元件將繼續展示原來的好友狀態。這是一個 bug。而且我們還會因為取消訂閱時使用錯誤的好友 ID 導致記憶體洩露或崩潰的問題。
在 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 應用中常見的 bug 來源。
現在看一下使用 Hook 的版本:
function FriendStatus(props) { // ... useEffect(() => { // ... ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
它並不會受到此 bug 影響。(雖然我們沒有對它做任何改動。)
並不需要特定的程式碼來處理更新邏輯,因為 useEffect
預設就會處理。它會在呼叫一個新的 effect
之前對前一個 effect
進行清理。為了說明這一點,下面按時間列出一個可能會產生的訂閱和取消訂閱操作呼叫序列:
// 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
可能會導致效能問題。在 class
元件中,我們可以通過在 componentDidUpdate
中新增對 prevProps
或 prevState
的比較邏輯解決:
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }
這是很常見的需求,所以它被內建到了 useEffect
的 Hook API 中。如果某些特定值在兩次重渲染之間沒有發生變化,你可以通知 React 跳過對 effect
的呼叫,只要傳遞陣列作為 useEffect
的第二個可選引數即可:
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 僅在 count 更改時更新
上面這個範例中,我們傳入 [count]
作為第二個引數。這個引數是什麼作用呢?如果 count
的值是 5
,而且我們的元件重渲染的時候 count
還是等於 5
,React
將對前一次渲染的 [5]
和後一次渲染的 [5]
進行比較。因為陣列中的所有元素都是相等的 (5 === 5)
,React 會跳過這個 effect,這就實現了效能的優化。
當渲染時,如果 count
的值更新成了 6
,React 將會把前一次渲染時的陣列 [5]
和這次渲染的陣列 [6]
中的元素進行對比。這次因為 5 !== 6
,React 就會再次呼叫 effect
。如果陣列中有多個元素,即使只有一個元素髮生變化,React 也會執行 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 發生變化時,重新訂閱
未來版本,可能會在構建時自動新增第二個引數。
注意:
如果你要使用此優化方式,請確保陣列中包含了所有外部作用域中會隨時間變化並且在 effect
中使用的變數,否則你的程式碼會參照到先前渲染中的舊變數。參閱檔案,瞭解更多關於如何處理常式以及陣列頻繁變化時的措施內容。
如果想執行只執行一次的 effect(僅在元件掛載和解除安裝時執行),可以傳遞一個空陣列([]
)作為第二個引數。這就告訴 React 你的 effect
不依賴於 props
或 state
中的任何值,所以它永遠都不需要重複執行。這並不屬於特殊情況 —— 它依然遵循依賴陣列的工作方式。
如果你傳入了一個空陣列([]
),effect
內部的 props
和 state
就會一直擁有其初始值。儘管傳入 []
作為第二個引數更接近大家更熟悉的 componentDidMount
和 componentWillUnmount
思維模式,但我們有更好的方式來避免過於頻繁的重複呼叫 effect
。除此之外,請記得 React 會等待瀏覽器完成畫面渲染之後才會延遲呼叫 useEffect
,因此會使得額外操作很方便。
我們推薦啟用 eslint-plugin-react-hooks
中的 exhaustive-deps
規則。此規則會在新增錯誤依賴時發出警告並給出修復建議。
本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45