- 深入理解React Router:從原理到實踐
- 李楊韜
- 2454字
- 2021-04-16 16:10:50
3.2.2 useEffect
1.副作用
在計算機科學中,如果某些操作、函數或表達式在其局部環境之外修改了一些狀態變量值,則稱其具有副作用(side effect)。副作用可以是一個與第三方通信的網絡請求,或者是外部變量的修改,或者是調用具有副作用的任何其他函數。副作用并無好壞之分,其存在可能影響其他環境的使用,開發者需要做的是正確處理副作用,使得副作用操作與程序的其余部分隔離,這將使得整個軟件系統易于擴展、重構、調試、測試和維護。在大多數前端框架中,也鼓勵開發者在單獨的、松耦合的模塊中管理副作用和組件渲染。
對于函數來說,無副作用執行的函數稱為純函數,它們接收參數,并返回值。純函數是確定性的,意味著在給定輸入的情況下,它們總是返回相同的輸出。但這并不意味著所有非純函數都具有副作用,如在函數內生成隨機值會使純函數變為非純函數,但不具有副作用。
React是關于純函數的,它要求render純凈。若render不純凈,則會影響其他組件,影響渲染。但在瀏覽器中,副作用無處不在,如果希望在React中處理副作用,則可使用useEffect。useEffect,顧名思義,就是執行有副作用的操作,其聲明如下:

函數的第一個參數為副作用函數,第二個參數為執行副作用的依賴數組,這將在下面的內容中介紹。
示例如下:


當上述組件初始化后,在打印render后會打印一次color effect,表明組件渲染之后,執行了傳入的effect。而在單擊ID為content的元素后,將更新value狀態,觸發一次渲染,打印render之后會打印color effect red。這一流程表明React的DOM已經更新完畢,并將控制權交給開發者的副作用函數,副作用函數成功地獲取到了DOM更新后的值。事實上,上述流程與React的componentDidMount、componentDidUpdate生命周期類似,React首次渲染和之后的每次渲染都會調用一遍傳給useEffect的函數,這也是useEffect與傳統類組件可以類比的地方。一般來說,useEffect可類比為componentDidMount、componentDidUpdate、componentWillUnmount三者的集合,但要注意它們不完全等同,主要區別在于componentDidMount或componentDidUpdate中的代碼是“同步”執行的。這里的“同步”指的是副作用的執行將阻礙瀏覽器自身的渲染,如有時候需要先根據DOM計算出某個元素的尺寸再重新渲染,這時候生命周期方法會在瀏覽器真正繪制前發生。
而useEffect中定義的副作用函數的執行不會阻礙瀏覽器更新視圖,也就是說這些函數是異步執行的。所謂異步執行,指的是傳入useEffect的回調函數是在瀏覽器的“繪制”階段之后觸發的,不“同步”阻礙瀏覽器的繪制。在通常情況下,這是比較合理的,因為大多數的副作用都沒有必要阻礙瀏覽器的繪制。對于useEffect,React使用了一種特殊手段保證effect函數在“繪制”階段后觸發:


requestAnimationFrame與postMessage結合使用以達到這一類目的。
簡而言之,useEffect會在瀏覽器執行完reflow/repaint流程之后觸發,effect函數適合執行無DOM依賴、不阻礙主線程渲染的副作用,如數據網絡請求、外部事件綁定等。
2.清除副作用
當副作用對外界產生某些影響時,在再次執行副作用前,應先清除之前的副作用,再重新更新副作用,這種情況可以在effect中返回一個函數,即cleanup(清除)函數。
每個effect都可以返回一個清除函數。作為useEffect可選的清除機制,其可以將監聽和取消監聽的邏輯放在一個effect中。
那么,React何時清除effect?effect的清除函數將會在組件重新渲染之后,并先于副作用函數執行。以一個例子來說明:

每次單擊div元素,都會打印:


如上例所示,React會在執行當前 effect 之前對上一個 effect 進行清除。清除函數作用域中的變量值都為上一次渲染時的變量值,這與Hooks的Caputure Value特性有關,將在下面的內容中介紹。
除了每次更新會執行清除函數,React還會在組件卸載的時候執行清除函數。
3.減少不必要的effect
如上面內容所說,在每次組件渲染后,都會運行effect中的清除函數及對應的副作用函數。若每次重新渲染都執行一遍這些函數,則顯然不夠經濟,在某些情況下甚至會造成副作用的死循環。這時,可利用useEffect參數列表中的第二個參數解決。useEffect參數列表中的第二個參數也稱為依賴列表,其作用是告訴React只有當這個列表中的參數值發生改變時,才執行傳入的副作用函數:

那么,React是如何判斷依賴列表中的值發生了變化的呢?事實上,React對依賴列表中的每個值,將通過Object.is進行元素前后之間的比較,以確定是否有任何更改。如果在當前渲染過程中,依賴列表中的某一個元素與該元素在上一個渲染周期的不同,則將執行effect副作用。
注意,如果元素之一是對象或數組,那么由于Object.is將比較對象或數組的引用,因此可能會造成一些疑惑:


如果config每次都由外部傳入,那么盡管config對象的字段值都不變,但由于新傳入的對象與之前config對象的引用不相等,因此effect副作用將被執行。要解決此種問題,可以依賴一些社區的解決方案,如use-deep-compare-effect。
在通常情況下,若useEffect的第二個參數傳入一個空數組[](這并不屬于特殊情況,它依然遵循依賴列表的工作方式),則React將認為其依賴元素為空,每次渲染比對,空數組與空數組都沒有任何變化。React認為effect不依賴于props或state中的任何值,所以effect副作用永遠都不需要重復執行,可理解為componentDidUpdate永遠不會執行。這相當于只在首次渲染的時候執行effect,以及在銷毀組件的時候執行cleanup函數。要注意,這僅是便于理解的類比,對于第二個參數傳入一個空數組[]與這類生命周期的區別,可查看下面的注意事項。
4.注意事項
1)Capture Value特性
注意,React Hooks有著Capture Value的特性,每一次渲染都有它自己的props和state:

在useEffect中,獲得的永遠是初始值0,將永遠打印“count is 0”;h1中的值也將永遠為setCount(0+1)的值,即“1”。若希望count能依次增加,則可使用useRef保存count,useRef將在3.2.4節介紹。
2)async函數
useEffect不允許傳入async函數,如:


原因在于async函數返回了promise,這與useEffect的cleanup函數容易混淆。在async函數中返回cleanup函數將不起作用,若要使用async函數,則可進行如下改寫:

3)空數組依賴
注意,useEffect傳遞空數組依賴容易產生一些問題,這些問題通常容易被忽視,如以下示例:

單擊“銷毀Child組件”按鈕,瀏覽器將彈出“componentWillUnmount and count is 0”提示框,無論setCount被調用多少次,都將如此,這是由Capture Value特性所導致的。而類組件的componentWillUnmount生命周期可從this.props.count中獲取到最新的count值。
在使用useEffect時,注意其不完全與componentDidUpdate、componentWillUnmount等生命周期等同,應該以“副作用”或狀態同步的方式去思考useEffect。但這也不代表不建議使用空數組依賴,需要結合上下文場景決定。與其將useEffect視為一個功能來經歷3個單獨的生命周期,不如將其簡單地視為一種在渲染后運行副作用的方式,可能會更有幫助。
useEffect的設計意圖是關注數據流的改變,然后決定effect該如何執行,與生命周期的思考模型需要區分開。
- 自然語言處理實戰:預訓練模型應用及其產品化
- 深入淺出Electron:原理、工程與實踐
- 零基礎學Scratch少兒編程:小學課本中的Scratch創意編程
- 匯編語言程序設計(第3版)
- Hands-On Nuxt.js Web Development
- 零基礎學C語言程序設計
- Troubleshooting Citrix XenApp?
- Backbone.js Testing
- Qt 4開發實踐
- Visual C++開發寶典
- Implementing Microsoft Dynamics NAV(Third Edition)
- 網頁設計與制作
- 青少年Python趣味編程
- MySQL核心技術與最佳實踐
- 片上系統設計思想與源代碼分析