React Hook
useEffect 就是一個 Effect Hook,給函數組件增加了操作副作用的能力。
它跟 class 組件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不過被合并成了一個 API。
1. 無需清除的effect
// 在組件didMount和didUpdate的時候 都會執行該方法
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>
);
}
2. useEffect 做了什么?
通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執行某些操作。React 會保存你傳遞的函數(我們將它稱之為 “effect”),并且在執行 DOM 更新之后調用它。在這個 effect 中,我們設置了 document 的 title 屬性,不過我們也可以執行數據獲取或調用其他命令式的 API。
3. 為什么在組件內部調用 useEffect?
將 useEffect 放在組件內部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經保存在函數作用域中。
Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供了解決方案的情況下,還引入特定的 React API。
4. useEffect 會在每次渲染后都執行嗎?
是的,默認情況下,它在第一次渲染之后*****和*****每次更新之后都會執行。(我們稍后會談到如何控制它。)你可能會更容易接受 effect 發生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。
React 保證了每次運行 effect 的同時,DOM 都已經更新完畢。
傳遞給 useEffect 的函數在每次渲染中都會有所不同,這是刻意為之的。事實上這正是我們可以在 effect 中獲取最新的 count 的值,而不用擔心其過期的原因。
每次我們重新渲染,都會生成新的 effect,替換掉之前的。某種意義上講,effect 更像是渲染結果的一部分 —— 每個 effect “屬于”一次特定的渲染。
5. 需要清除的effect
為什么要在 effect 中返回一個函數?
這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬于 effect 的一部分。
React 何時清除 effect?
React 會在組件卸載的時候執行清除操作。effect 在每次渲染的時候都會執行。
這就是為什么 React *會*在執行當前 effect 之前對上一個 effect 進行清除。我們稍后將討論[為什么這將助于避免 bug](https://react.docschina.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update)以及[如何在遇到性能問題時跳過此行為](https://react.docschina.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects)。
Hook 允許我們按照代碼的用途分離他們, 而不是像生命周期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的每一個 effect。
6. 解釋: 為什么每次更新的時候都要運行 Effect
如果你已經習慣了使用 class,那么你或許會疑惑為什么 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
通過Effect進行性能優化
在某些情況下,每次渲染后都執行清理或者執行 effect 可能會導致性能問題。在 class 組件中,我們可以通過在 componentDidUpdate 中添加對 prevProps 或 prevState 的比較邏輯解決:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
通知 React 跳過對 effect 的調用,只要傳遞數組作為 useEffect 的第二個可選參數即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新
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 中的任何值,所以它永遠都不需要重復執行。這并不屬于特殊情況 —— 它依然遵循依賴數組的工作方式。
推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告并給出修復建議。