響應式 Effect 的生命週期
Effect 的生命週期與元件不同。元件可能會掛載、更新或卸載。一個 Effect 只能做兩件事:開始同步某些東西,然後稍後停止同步它。如果你的 Effect 依賴於隨時間變化的 props 和 state,這個循環可能會發生多次。React 提供了一個 linter 規則來檢查你是否正確指定了 Effect 的依賴項。這使你的 Effect 與最新的 props 和 state 保持同步。
您將學習
- Effect 的生命週期與元件的生命週期有何不同
- 如何獨立思考每個單獨的 Effect
- 你的 Effect 何時需要重新同步,以及原因
- 如何確定你的 Effect 的依賴項
- 值具有響應性意味著什麼
- 空依賴陣列意味著什麼
- React 如何透過 linter 驗證你的依賴項是否正確
- 當你不同意 linter 時該怎麼辦
Effect 的生命週期
每個 React 元件都經歷相同的生命週期:
- 當元件被添加到螢幕時,它會掛載。
- 當元件接收到新的 props 或 state(通常是為了回應互動)時,它會更新。
- 當元件從螢幕上移除時,它會卸載。
這是思考元件的好方法,但 不適用於 Effect。相反,請嘗試獨立於元件的生命週期來思考每個 Effect。Effect 描述瞭如何將外部系統與當前的 props 和 state 同步。隨著你的程式碼變化,同步發生的頻率會增加或減少。
為了說明這一點,請考慮這個將你的元件連接到聊天伺服器的 Effect:
你的 Effect 主體指定瞭如何開始同步:
你的 Effect 返回的清理函數指定瞭如何停止同步:
直覺上,你可能會認為 React 會在元件掛載時開始同步,並在元件卸載時 停止同步。然而,這並不是故事的全部!有時,在元件保持掛載的同時,也可能需要 多次開始和停止同步。
讓我們看看為什麼這是必要的,何時會發生,以及如何控制這種行為。
注意
有些 Effect 根本不返回清理函數。大多數情況下,你會希望返回一個——但如果你不返回,React 會表現得好像你返回了一個空的清理函數。
為什麼同步可能需要發生多次
想像這個ChatRoom元件接收一個用戶在下拉選單中選擇的roomId prop。假設最初用戶選擇 "general"房間作為roomId。你的應用程式顯示"general"聊天室:
UI 顯示後,React 將運行你的 Effect 以開始同步。它連接到 "general"房間:
到目前為止,一切順利。
稍後,使用者在下拉式選單中選擇了不同的聊天室(例如"travel")。首先,React 會更新 UI:
思考一下接下來應該發生什麼。使用者看到 UI 中選中的聊天室是"travel"。然而,上一次執行的 Effect 仍然連接著 "general"聊天室。 roomId屬性已經改變,因此你的 Effect 之前所做的(連接到"general"聊天室)不再與 UI 相符。
此時,你希望 React 做兩件事:
- 停止與舊的
roomId同步(從"general"聊天室斷開連接) - 開始與新的
roomId同步(連接到"travel"聊天室)
幸運的是,你已經教會 React 如何做這兩件事!你的 Effect 主體指定了如何開始同步,而你的清理函數指定了如何停止同步。現在 React 只需要以正確的順序並使用正確的屬性和狀態來呼叫它們。讓我們看看這具體是如何發生的。
React 如何重新同步你的 Effect
回想一下,你的ChatRoom 元件已經收到了其 roomId屬性的一個新值。它曾經是"general",而現在是"travel"。React 需要重新同步你的 Effect,以將你重新連接到不同的聊天室。
為了停止同步, React 將呼叫你的 Effect 在連接到 "general"聊天室後返回的清理函數。由於roomId 是 "general",清理函數會從 "general"聊天室斷開連接:
然後 React 將執行你在這次渲染期間提供的 Effect。這次,roomId 是 "travel",因此它將開始同步到 "travel" 聊天室(直到其清理函數最終也被呼叫):
得益於此,你現在已連接到使用者在 UI 中選擇的同一個聊天室。避免了災難!
每次你的元件以不同的 roomId重新渲染後,你的 Effect 都會重新同步。例如,假設使用者將roomId 從 "travel"更改為"music"。React 將再次停止同步你的 Effect,方法是呼叫其清理函數(將你從"travel"聊天室斷開連接)。然後它將開始同步,使用新的 roomId屬性執行其主體(將你連接到"music"聊天室)。
最後,當使用者前往不同的畫面時,ChatRoom會解除安裝。現在完全沒有必要保持連接。React 將停止同步你的 Effect 最後一次,並將你從"music"聊天室斷開連接。
從 Effect 的角度思考
讓我們從 ChatRoom元件的角度來總結一下發生的一切:
ChatRoom掛載時,roomId設為"general"ChatRoom更新時,roomId設為"travel"ChatRoom更新時,roomId設為"music"ChatRoom卸載
在元件生命週期的這些時間點,你的 Effect 執行了不同的操作:
- 你的 Effect 連線到
"general"聊天室 - 你的 Effect 從
"general"聊天室斷開連線,並連線到"travel"聊天室 - 你的 Effect 從
"travel"聊天室斷開連線,並連線到"music"聊天室 - 你的 Effect 從
"music"聊天室斷開連線
現在,讓我們從 Effect 自身的角度來思考發生了什麼:
這段程式碼的結構可能會啟發你,將發生的事情視為一系列不重疊的時間段:
- 你的 Effect 連線到
"general"聊天室(直到斷開連線) - 你的 Effect 連線到
"travel"聊天室(直到斷開連線) - 你的 Effect 連線到
"music"聊天室(直到斷開連線)
之前,你是從元件的角度思考。當你從元件的角度來看時,很容易將 Effects 視為在特定時間點觸發的「回調函數」或「生命週期事件」,例如「渲染後」或「卸載前」。這種思考方式很快就會變得複雜,因此最好避免。
相反地,請始終專注於單一的開始/停止週期。元件是正在掛載、更新還是卸載,都不應該影響你的思考。你需要做的只是描述如何開始同步以及如何停止它。如果你做得好,你的 Effect 將能夠根據需要被啟動和停止任意次數。
這可能會讓你想起,當你編寫建立 JSX 的渲染邏輯時,你並不會考慮元件是正在掛載還是更新。你描述螢幕上應該顯示什麼,而 React 會處理其餘的事情。
React 如何驗證你的 Effect 可以重新同步
這裡有一個你可以實際操作的範例。點擊「開啟聊天」來掛載ChatRoom 元件:
請注意,當元件首次掛載時,你會看到三條日誌:
✅ Connecting to "general" room at https://localhost:1234...(僅限開發環境)❌ Disconnected from "general" room at https://localhost:1234.(僅限開發環境)✅ Connecting to "general" room at https://localhost:1234...
前兩條日誌僅限於開發環境。在開發環境中,React 總是會重新掛載每個元件一次。
React 透過在開發環境中強制立即執行來驗證你的 Effect 能夠重新同步。這可能會讓你想起開門後再關上一次,以檢查門鎖是否正常運作。React 在開發環境中會額外啟動和停止你的 Effect 一次,以檢查你是否妥善實作了其清理函式。
在實際應用中,你的 Effect 需要重新同步的主要原因是它所使用的某些資料發生了變化。在上面的沙盒中,更改選中的聊天室。請注意,當roomId改變時,你的 Effect 會重新同步。
然而,也存在一些更特殊的情況需要重新同步。例如,在上面的沙盒中,當聊天開啟時,嘗試編輯serverUrl。請注意 Effect 如何響應你對程式碼的編輯而重新同步。未來,React 可能會新增更多依賴於重新同步的功能。
React 如何知道需要重新同步 Effect
你可能會好奇,React 是如何知道在 roomId改變後,你的 Effect 需要重新同步。這是因為你告訴了 React 其程式碼依賴於 roomId,方法是將其包含在依賴項清單中:
其運作原理如下:
- 你知道
roomId是一個屬性,這意味著它可能會隨時間改變。 - 你知道你的 Effect 讀取了
roomId(因此其邏輯依賴於一個未來可能改變的值)。 - 這就是你將其指定為 Effect 依賴項的原因(以便在
roomId改變時重新同步)。
每次你的元件重新渲染後,React 都會查看你傳遞的依賴項陣列。如果陣列中的任何值與你在上一次渲染時傳遞的相同位置的值不同,React 就會重新同步你的 Effect。
例如,如果你在初始渲染時傳遞了["general"],之後在下一次渲染時傳遞了 ["travel"],React 會比較 "general" 和 "travel"。這些是不同的值(使用 Object.is比較),因此 React 會重新同步你的 Effect。另一方面,如果你的元件重新渲染但roomId沒有改變,你的 Effect 將保持連線到同一個聊天室。
每個 Effect 代表一個獨立的同步過程
請勿僅僅因為某個邏輯需要與你已編寫的 Effect 同時執行,就將不相關的邏輯加入該 Effect。例如,假設你想在使用者訪問聊天室時傳送一個分析事件。你已經有一個依賴於roomId的 Effect,因此你可能會想將分析呼叫加入其中:
但想像一下,你後來為這個 Effect 新增了另一個需要重新建立連線的依賴項。如果這個 Effect 重新同步,它也會為同一個聊天室呼叫logVisit(roomId),這並非你的本意。記錄訪問 是一個獨立的過程,與建立連線不同。將它們寫成兩個獨立的 Effect:
你程式碼中的每個 Effect 都應該代表一個獨立且分離的同步過程。
在上面的例子中,刪除一個 Effect 不會破壞另一個 Effect 的邏輯。這是一個很好的跡象,表明它們同步的是不同的事物,因此將它們分開是有道理的。另一方面,如果你將一個緊密關聯的邏輯片段拆分到不同的 Effect 中,程式碼可能看起來「更乾淨」,但會更難維護。這就是為什麼你應該考慮過程是相同還是分離,而不是程式碼看起來是否更乾淨。
Effect 對「響應式」值做出反應
你的 Effect 讀取了兩個變數(serverUrl 和 roomId),但你只將 roomId指定為依賴項:
為什麼 serverUrl不需要作為依賴項?
這是因為 serverUrl不會因為重新渲染而改變。無論元件重新渲染多少次以及原因為何,它始終保持不變。既然serverUrl永遠不會改變,將其指定為依賴項就沒有意義。畢竟,依賴項只有在隨時間變化時才會產生作用!
另一方面,roomId在重新渲染時可能會不同。在元件內部宣告的 props、state 和其他值都是響應式的,因為它們是在渲染期間計算的,並且參與了 React 的資料流。
如果serverUrl是一個狀態變數,那麼它就會是響應式的。響應式值必須包含在依賴項中:
透過將 serverUrl包含為依賴項,您可以確保 Effect 在其改變後重新同步。
請嘗試在此沙盒中更改選取的聊天室或編輯伺服器 URL:
每當您更改像 roomId 或 serverUrl這樣的響應式值時,Effect 就會重新連接到聊天伺服器。
具有空依賴項的 Effect 意味著什麼
如果您將 serverUrl 和 roomId都移到元件外部會發生什麼?
現在您的 Effect 程式碼沒有使用任何響應式值,因此其依賴項可以為空([])。
從元件的角度思考,空的 []依賴陣列意味著這個 Effect 僅在元件掛載時連接到聊天室,並在元件卸載時斷開連接。(請記住,React 在開發環境中仍會額外重新同步一次 以壓力測試您的邏輯。)
然而,如果您從 Effect 的角度思考,您根本不需要考慮掛載和卸載。重要的是您已經指定了您的 Effect 開始和停止同步的動作。目前,它沒有響應式依賴項。但如果您希望使用者隨時間更改roomId 或 serverUrl(它們將變成響應式),您的 Effect 程式碼不會改變。您只需要將它們添加到依賴項中即可。
在元件主體中宣告的所有變數都是響應式的
Props 和 state 並非唯一的響應式值。從它們計算出來的值也是響應式的。如果 props 或 state 改變,您的元件將重新渲染,從它們計算出來的值也會改變。這就是為什麼元件主體中所有被 Effect 使用的變數都應該在 Effect 依賴列表中。
假設使用者可以在下拉選單中選擇聊天伺服器,但他們也可以在設定中配置預設伺服器。假設您已經將設定狀態放入 context中,因此您從該 context 讀取settings。現在您根據從 props 中選取的伺服器和預設伺服器計算serverUrl:
在這個例子中,serverUrl不是一個 prop 或狀態變數。它是一個你在渲染期間計算的普通變數。但因為它是在渲染期間計算的,所以它可能因為重新渲染而改變。這就是為什麼它是響應式的。
元件內部的所有值(包括 props、狀態以及元件主體中的變數)都是響應式的。任何響應式值都可能因重新渲染而改變,因此你需要將響應式值包含為 Effect 的依賴項。
換句話說,Effect 會「響應」來自元件主體的所有值。
React 驗證你是否將每個響應式值都指定為依賴項
如果你的 linter 已為 React 配置,它將檢查你的 Effect 程式碼中使用的每個響應式值是否都宣告為其依賴項。例如,這是一個 lint 錯誤,因為roomId 和 serverUrl都是響應式的:
這看起來可能像是 React 的錯誤,但實際上 React 是在指出你程式碼中的一個錯誤。roomId 和 serverUrl都可能隨時間改變,但你忘記在它們改變時重新同步你的 Effect。即使使用者在 UI 中選擇了不同的值,你仍將保持連接到初始的roomId 和 serverUrl。
要修復這個錯誤,請遵循 linter 的建議,將roomId 和 serverUrl指定為你的 Effect 的依賴項:
在上面的沙盒中嘗試這個修復。確認 linter 錯誤已消失,並且聊天在需要時會重新連接。
當你不想重新同步時該怎麼做
在前面的例子中,你通過列出 roomId 和 serverUrl作為依賴項來修復了 lint 錯誤。
然而,你也可以選擇向 linter「證明」這些值不是響應式值,也就是說,它們不可能 因重新渲染而改變。例如,如果 serverUrl 和 roomId不依賴於渲染且始終具有相同的值,你可以將它們移到元件外部。現在它們就不需要成為依賴項了:
你也可以將它們移到 Effect 內部。它們不是在渲染期間計算的,因此不具有響應性:
Effect 是響應式的程式碼區塊。當你在其中讀取的值發生變化時,它們會重新同步。與事件處理器(每次互動只執行一次)不同,Effect 會在需要同步時執行。
你不能「選擇」你的依賴項。你的依賴項必須包含 Effect 中讀取的每一個響應式值。linter 會強制執行這一點。有時這可能會導致無限迴圈或 Effect 過於頻繁地重新同步等問題。不要透過抑制 linter 來解決這些問題!以下是你可以嘗試的方法:
- 檢查你的 Effect 是否代表一個獨立的同步過程。如果你的 Effect 沒有同步任何東西,它可能是不必要的。如果它同步了幾個獨立的東西,請將其拆分。
- 如果你想讀取 props 或 state 的最新值,但不想「響應」它並讓 Effect 重新同步,你可以將 Effect 拆分為響應式部分(保留在 Effect 中)和非響應式部分(提取到稱為Effect Event的東西中)。閱讀關於將事件與 Effect 分離的內容。
- 避免依賴物件和函式作為依賴項。如果你在渲染期間建立物件和函式,然後從 Effect 中讀取它們,它們在每次渲染時都會不同。這將導致你的 Effect 每次都會重新同步。閱讀更多關於從 Effect 中移除不必要依賴項的內容。
總結
- 元件可以掛載、更新和卸載。
- 每個 Effect 都有獨立於周圍元件的生命週期。
- 每個 Effect 描述了一個可以開始和停止的獨立同步過程。
- 當你編寫和閱讀 Effect 時,請從每個獨立 Effect 的角度思考(如何開始和停止同步),而不是從元件的角度(它如何掛載、更新或卸載)。
- 在元件主體中宣告的值是「響應式」的。
- 響應式值應該重新同步 Effect,因為它們會隨時間變化。
- linter 會驗證 Effect 內部使用的所有響應式值是否都被指定為依賴項。
- linter 標記的所有錯誤都是合理的。總有一種方法可以修復程式碼而不違反規則。
Try out some challenges
Challenge 1 of 5:Fix reconnecting on every keystroke #
In this example, the ChatRoom component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working.
However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesn’t happen.
