React Hook:使用 useEffect
一楔脯、描述
Effect Hook 可以讓你能夠在 Function 組件中執(zhí)行副作用(side effects):
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
// 類似于 componentDidMount 和 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>
);
}
上面代碼看過很多次了,是 React 文檔中 Hook 部分一直使用的計(jì)數(shù)器示例,但是多了個(gè)新的功能:把文檔標(biāo)題設(shè)置為包含點(diǎn)擊次數(shù)的自定義消息。而這就是一個(gè)副作用迷守。
數(shù)據(jù)獲取哀峻,設(shè)置訂閱或者手動(dòng)直接更改 React 組件中的 DOM 都屬于副作用。有的人習(xí)慣成這種行為為 effects
刹勃,我是比較習(xí)慣叫 side effects
也就是副作用的, 這是個(gè)概念嚎尤,需要在 React 必須習(xí)慣的概念荔仁。
如果熟悉 React 類聲明周期方法,可以把 useEffect
Hook 視作 componentDidMount
芽死、componentDidUpdate
和 componentWillUnmount
的組合體乏梁。
React 組件中有兩種常見的副作用:
- 需要清理的副作用
- 不需要清理的副作用。
二关贵、需要清理的副作用
有的時(shí)候遇骑,我們希望在 React 更新 DOM 之后進(jìn)行一些額外的操作。網(wǎng)絡(luò)請(qǐng)求揖曾、手動(dòng)更改 DOM 以及日志記錄都是不需要清理的副作用的常見場景落萎。因?yàn)檫\(yùn)行之后,可以立即被銷毀掉炭剪。
下面是在 class 組件和 function 組件中分別表示這兩種副作用的使用方式:
1练链、在 class 組件中
在 React class 組件中, render 方法本身不應(yīng)該進(jìn)行副作用操作奴拦,但是我們通常是期望在 React 更新 DOM 之后執(zhí)行一些有必要的副作用媒鼓。
這就是為什么在 React class 中,會(huì)把副作用放在 componentDidMount
和 componentDidUpdate
中错妖÷堂回到計(jì)數(shù)器的示例中,如果要在 class 計(jì)數(shù)器組件中實(shí)現(xiàn)上面的功能暂氯,則代碼如下:
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 組件中,兩個(gè)生命周期中有相同的代碼(雖然 componentDidUpdate 中的內(nèi)容也可以放在 click 的事件 handler 中)
這是因?yàn)樵诙鄶?shù)情況下株旷,我們希望執(zhí)行相同的副作用再登,無論是組件剛 mount 還是 update 之后尔邓。而從概念上來講,我們希望他在每次 render 之后發(fā)生锉矢,但是 React 類組件是沒有這種生命周期的梯嗽。雖然可以把 document.title = 'You clicked' + this.state.count + ' times';
這個(gè)操作封裝到一個(gè)方法中,但是還是需要在 componentDidMount
和 componentDidUpdate
中調(diào)用兩次沽损。
2灯节、使用 effect Hook 的示例
文章最頂部已經(jīng)寫了下面的示例,為了分析代碼绵估,單獨(dú)再拿到這里:
import { 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>
);
}
1炎疆、useEffect
做了什么?
通過使用這個(gè) Hook国裳,通知 React 組件需要在渲染后執(zhí)行什么操作形入。React 將記住傳遞的 function(把這個(gè) function 成為 “effect”),并在執(zhí)行 DOM 更新后調(diào)用這個(gè) function缝左。在這個(gè)效果中亿遂,主要的功能仍舊是設(shè)置 document.title
,但是也可以執(zhí)行數(shù)據(jù)獲取,或者是調(diào)用其他的命令式的 API渺杉。
2蛇数、為什么在組件內(nèi)調(diào)用 useEffect
?
在組件內(nèi)使用 useEffect
是的可以直接從副作用中訪問計(jì)數(shù)器的 count
或者任何的 props是越。不需要使用特殊的 API 來讀取它耳舅,它已經(jīng)在函數(shù)的范圍內(nèi)了(通過 useState
)。Hooks 擁抱 Javascript 的閉包倚评,并且避免在 Javascript 已經(jīng)提供解決方案的情況下在去引入特定的 React API浦徊。
3、每次 render 之后都會(huì)執(zhí)行 useEffect 嗎蔓纠?
是的辑畦!
這是默認(rèn)行為,在第一次 render 之后和每次 update 之后都會(huì)運(yùn)行腿倚。你可能會(huì)更容易的認(rèn)為副作用發(fā)生在 “render 之后”,而不是發(fā)生在 “mount” 和 “update” 之后蚯妇。不過 React 保證 DOM 在運(yùn)行時(shí)副作用已經(jīng)更新敷燎。
(網(wǎng)絡(luò)請(qǐng)求每次都放在這里面肯定是有問題的,因此需要定制)如果要定制 useEffect
的默認(rèn)執(zhí)行行為箩言,可以參考:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
3硬贯、詳細(xì)代碼拆分說明
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
我們通過 useState
聲明了 count
state 變量,并且通知 React 需要使用 effect陨收。
然后把一個(gè) funcrion 傳遞給 useEffect
Hook饭豹,而傳遞的這個(gè) funcrion 就是副作用鸵赖。
在我們的副作用中,使用 document.title
瀏覽器 API 設(shè)置文檔的標(biāo)題拄衰,可以在 effect 中讀取最新的 count它褪,因?yàn)?count 變量作用域就是在整個(gè) Example function 中。當(dāng) React 渲染我們的組件時(shí)翘悉,會(huì)機(jī)主我們使用的 effect茫打,然后在更新 DOM 后運(yùn)行需要的下溝哦。每次渲染都會(huì)發(fā)生這樣的情況妖混,包括第一次 render老赤。
你可能會(huì)注意到,傳遞給 useEffect
的 function 在每次 render 的時(shí)候有所不同制市,這是故意為之的抬旺。事實(shí)上,這就是讓我們?cè)诟弊饔弥凶x取 count 值而不需要擔(dān)心這個(gè)值是舊值祥楣。每次在 re-render 的時(shí)候开财,都會(huì)有一個(gè)不同的副作用,來取代之前的副作用荣堰。在某種程度上床未,這使得副作用更像是 render 結(jié)果的一部分——每個(gè)副作用都“屬于”特殊的 render。文章后面會(huì)提到為什么這是有用的振坚。
Tip
與 componentDidMount
和 componentDidUpdate
不同薇搁,使用 useEffect
調(diào)度的副作用不會(huì)阻塞瀏覽器更新屏幕。這使得 application 感覺上具有響應(yīng)式渡八。大多數(shù)副作用不需要同步發(fā)生啃洋。而如果需要同步進(jìn)行,(比如測(cè)量布局)屎鳍,有一個(gè)單獨(dú)的 useLayoutEffect Hook宏娄, API 和 useEffect
相同。
三逮壁、需要清理的副作用
上面都是不需要清理的副作用孵坚,然而,有些副作用是需要去清理的窥淆。比如卖宠,肯呢過希望設(shè)置對(duì)某些外部數(shù)據(jù)源的 subscription。而在這種情況下忧饭,清理訂閱是非常重要的扛伍,這樣不會(huì)引入內(nèi)存泄露。
1词裤、使用 class 組件示例:
在 React class 中刺洒,通常會(huì)在 componentDidMount
中設(shè)置帝國與鳖宾,而在 componentWillUnmount
中清楚它。比如有一個(gè) ChatAPI
模塊逆航,可以訂閱好友的在線狀態(tài)鼎文,在 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';
}
請(qǐng)注意 componentDidMount
和 componentWillMount
需要相互對(duì)應(yīng)。class 組件的生命周期強(qiáng)制我們?nèi)ゲ鸱诌@個(gè)邏輯纸泡,即使他們中的概念代碼和相同的副作用是有關(guān)的漂问。
注意
眼里好的人可能注意到了上面的示例可能需要一個(gè) componentDidUpdate
才能完全的正確,目前暫時(shí)忽略女揭。
2蚤假、使用 Hooks 的示例
一開始,可能會(huì)認(rèn)為需要單獨(dú)的 effect 去清理吧兔,但是添加訂閱和刪除訂閱的代碼聯(lián)系非常緊密磷仰,因此 useEffect
旨在將它保持在一起。如果你的副作用返回一個(gè)方法境蔼,則 React 則在清理時(shí)運(yùn)行:
import { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
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';
}
1灶平、為什么從 effect 中返回一個(gè) function?
這是 effect 可選的清理機(jī)制箍土。每個(gè) effect 都可以返回一個(gè)在它之后清理的 function逢享。這使得我們能夠保持添加訂閱和刪除訂閱彼此接近的訂閱的邏輯。這同樣是 effect 的一部分吴藻。
2瞒爬、React 在什么時(shí)候清理?
當(dāng)組件卸載的時(shí)候沟堡,React 會(huì)執(zhí)行清理工作侧但。
然而,effect 會(huì)針對(duì)每個(gè) render 運(yùn)行而不僅僅是一次航罗,這就是 React 在下次運(yùn)行 effect 之前還清除前一個(gè) render effect 的原因禀横。
有兩個(gè)鏈接:
四、總結(jié)
我們已經(jīng)了解了 useEffect
能夠在組件 render 之后進(jìn)行不同類型的副作用粥血。某些 effect 可能需要清理柏锄,因此可以在 effect 中返回一個(gè) function:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
而有一些 side effect 可能沒有清理的過程,因此不需要返回任何內(nèi)容复亏。
useEffect(() => {
document.title = `You clicked ${count} times`;
});
通過 useEffect
绢彤,能夠?qū)⒅霸趦蓚€(gè)生命周期中的內(nèi)容整合到一個(gè) function 中。
五蜓耻、使用 effect 的 tips
1、Tips:使用多個(gè) effect 來分離問題
使用 Hook 的動(dòng)機(jī)中包括了 class 組件的生命周期將相關(guān)的邏輯拆分的問題的解決械巡,而在 Hook 的使用中刹淌,也能夠把多個(gè) effect 放在 function 組件饶氏。
下面是 class 組件的相關(guān)代碼:
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
});
}
請(qǐng)注意設(shè)置 document.title
的邏輯如何在 componentDidMount
和 componentDidUpdate
之前拆分。而訂閱的邏輯也在 componentDidMount
和 componentDidUpdate
之間傳播有勾。componentDidMount
包含兩個(gè)任務(wù)的代碼疹启。
使用 Hook 解決這個(gè)問題其實(shí)就像之前使用 useState
解決問題一樣,可以使用多個(gè) 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(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
}
Hooks 允許我們根據(jù)它正在做的事情而不是生命周期方法名稱來拆分代碼。React 將按照指定的順序應(yīng)用組件使用的每個(gè) effect雇逞。
2荤懂、說明:為什么 effect 在每次 update 都會(huì)運(yùn)行
如果你習(xí)慣了使用 class 組件,你可能想知道為什么每次 re-render 之后塘砸,effect 的清理都會(huì)執(zhí)行节仿,而不是在卸載過程中只執(zhí)行一次(打斷點(diǎn)就能知道)。
在 useState
的文章(http://www.ptbird.cn/react-hook-use-state-hook.html) 中有一個(gè) FriendStatus
來表示好友是否在線掉蔬,class 組件沖 this.props 中讀取 friend.id
廊宪,在組件 mount 之后,就訂閱朋友的狀態(tài)女轿,并在卸載期間取消訂閱:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是如果 friend
prop 在組件出現(xiàn)在屏幕上時(shí)發(fā)生了和變化箭启,又會(huì)發(fā)生什么呢?類組件將繼續(xù)顯示不同的朋友的在線狀態(tài)蛉迹,這是一個(gè)bug傅寡,并且因?yàn)槿∠嗛喪褂昧隋e(cuò)誤的 friend ID,卸載時(shí)還可能導(dǎo)致內(nèi)存泄露或崩潰婿禽。
因此在類組件中赏僧,需要添加 componentDidUpdate
來處理這種情況:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
忘記處理 componentDidUpdate
是 React 應(yīng)用程序中常見的錯(cuò)誤。
如果使用 Hook :
function FriendStatus(props) {
// ...
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
上面的代碼如果 this.props.friend
發(fā)生了變化扭倾,也不會(huì)受到影響淀零。
沒有用于處理 update 的特殊的代碼,因?yàn)槟J(rèn)情況下 useEffect
會(huì)處理它們膛壹。它們?cè)趹?yīng)用下一個(gè) effect 之前清楚之前的 effect驾中。為了說明這一點(diǎn),下面是一個(gè)訂閱和取消訂閱調(diào)用的序列模聋,這個(gè)組件可能隨著時(shí)間的推移產(chǎn)生:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
這種默認(rèn)行為確保了一致性肩民,并防止由于缺少 update 的處理邏輯而產(chǎn)生 class 組件中常見的錯(cuò)誤。
3链方、Tip:跳過 effect 優(yōu)化性能
在某些情況下持痰,每次 render 后清理或者使用 effect 可能會(huì)產(chǎn)生性能問題。在類組件中祟蚀,可以通過 componentDidUpdate
中編寫 prevProps
或 prevState
的額外比較來解決這個(gè)問題:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
這個(gè)要求很常見工窍,而這種方式已經(jīng)被內(nèi)置到 useEffect
Hook 的 API中割卖,如果在重新渲染之間沒有更新某些值,則可以告訴 React 跳過 effect患雏,為了實(shí)現(xiàn)這種方式鹏溯,需要將數(shù)組作為可選的第二個(gè)參數(shù)傳遞給 useEffect
:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);// 只有在 count 發(fā)生變化的時(shí)候才會(huì)執(zhí)行這個(gè) effect
上面的例子中, [count]
作為第二個(gè)參數(shù)傳遞淹仑。如果 count = 5
丙挽,然后組件如果進(jìn)行了 re-render,如果 count=5
匀借,則 React 會(huì)比較前一個(gè) render 和 下一個(gè) render 的值颜阐。因?yàn)閮纱?5 === 5
,因此React 會(huì)跳過這次 effect怀吻,這是性能優(yōu)化瞬浓。
當(dāng) count = 6
的時(shí)候,React 會(huì)比較 5 !== 6
蓬坡。此時(shí)猿棉,React 會(huì)重新去調(diào)用 effect,如果數(shù)組中有多個(gè)項(xiàng)目屑咳,只要有一個(gè)的比較值是不相同的萨赁, React 也會(huì)執(zhí)行這個(gè) effect。
上面的作用兆龙,也同樣應(yīng)用于 cleanup 的 effect:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 只有 props.friend.id 變化的時(shí)候才會(huì)調(diào)用 effect
未來杖爽,第二個(gè)參數(shù)可能會(huì)通過構(gòu)建的時(shí)候,轉(zhuǎn)換自動(dòng)添加紫皇。
注意
如果使用此優(yōu)化慰安,需要確保數(shù)組包含外部作用域隨時(shí)間變化且 effect 使用的任何值。否則聪铺,你的代碼將引用之前渲染的舊值化焕。在 https://reactjs.org/docs/hooks-reference.html 有又關(guān)于 Hooks 優(yōu)化的更多內(nèi)容。
如果要運(yùn)行效果并且僅將其清理一次(在 mount 和 unmount 的時(shí)候)铃剔,可以把空數(shù)組 []
作為第二個(gè)參數(shù)傳遞撒桨。這告訴React你的效果不依賴于來自props或state的任何值,所以它永遠(yuǎn)不需要重新運(yùn)行键兜。這不會(huì)作為特殊情況進(jìn)行處理 - 它直接遵循輸入數(shù)組的工作方式凤类。雖然傳遞 []
更接近 componentDidMount
和 componentWillUnmount
的模式,但是不建議將其作為一種習(xí)慣普气,如果存在訂閱的話谜疤,經(jīng)常會(huì)導(dǎo)致錯(cuò)誤。
不要忘記 React 會(huì)延遲運(yùn)行 useEffect
直到瀏覽器 render 之后,所以進(jìn)行額外的操作也不是問題茎截。
文章版權(quán):Postbird-There I am , in the world more exciting!
本文鏈接:http://www.ptbird.cn/react-hoot-useEffect.html
轉(zhuǎn)自http://www.ptbird.cn/react-hoot-useEffect.html#menu_index_1
轉(zhuǎn)載請(qǐng)注明文章原始出處 !