使用 React/Hooks 時(shí)需要注意過時(shí)的閉包!
本文已經(jīng)過原作者 Shadeed 授權(quán)翻譯。
Hooks 簡化了 React 組件內(nèi)部狀態(tài)和副作用的管理。此外,可以將重復(fù)的邏輯提取到自定義 Hooks 中,以在整個(gè)應(yīng)用程序中重復(fù)使用。
Hooks 嚴(yán)重依賴于 JS 閉包。這就是為什么 Hooks 如此具有表現(xiàn)力和簡單,但是閉包有時(shí)很棘手。
使用 Hooks 時(shí)可能遇到的一個(gè)問題就是過時(shí)的閉包,這可能很難解決。
讓我們從過時(shí)的裝飾開始。然后,看看到過時(shí)的閉包如何影響 React Hooks,以及如何解決該問題。
1.過時(shí)的閉包
工廠函數(shù) createIncrement(incBy) 返回一個(gè)increment和log函數(shù)的元組。調(diào)用時(shí),increment()函數(shù)將內(nèi)部value增加incBy,而log()僅打印一條消息,其中包含有關(guān)當(dāng)前value的信息:
- function createIncrement(incBy) {
- let value = 0;
- function increment() {
- value += incBy;
- console.log(value);
- }
- const message = `Current value is ${value}`; function log() { console.log(message); }
- return [increment, log];
- }
- const [increment, log] = createIncrement(1);
- increment(); // 1
- increment(); // 2
- increment(); // 3
- // 不能正確工作!
- log(); // "Current value is 0"
[increment, log] = createIncrement(1)返回一個(gè)函數(shù)元組:一個(gè)函數(shù)增加內(nèi)部值,另一個(gè)函數(shù)記錄當(dāng)前值。
然后,increment()的3次調(diào)用將 value遞增到3。
最后,log()調(diào)用打印消息是 Current value is 0,這有點(diǎn)出乎意料的,因?yàn)榇藭r(shí) value 為 3 了。
log()是一個(gè)過時(shí)的閉包。閉包 log()捕獲了值為 "Current value is 0"的 message變量。
即使 value 變量在調(diào)用increment()時(shí)被增加多次,message變量也不會更新,并且總是保持一個(gè)過時(shí)的值 "Current value is 0"。
過時(shí)的閉包捕獲具有過時(shí)值的變量。
2.修復(fù)過時(shí)的閉包
修復(fù)過時(shí)的log()問題需要關(guān)閉實(shí)際更改的變量:value的閉包。
我們將語句 const message = ...; 移動到 log() 函數(shù)內(nèi)部:
- function createIncrement(incBy) {
- let value = 0;
- function increment() {
- value += incBy;
- console.log(value);
- }
- function log() {
- const message = `Current value is ${value}`; console.log(message);
- }
- return [increment, log];
- }
- const [increment, log] = createIncrement(1);
- increment(); // 1
- increment(); // 2
- increment(); // 3
- // Works!
- log(); // "Current value is 3"
現(xiàn)在,在調(diào)用了 3 次 increment() 函數(shù)之后,調(diào)用 log() 記錄了實(shí)際value:"Current value is 3"。
3. Hooks 中的過時(shí)閉包
3.1 useEffect()
我們來看一下使用useEffect() 過時(shí)閉包的常見情況。
在組件中,useEffect() 中每2秒記錄一次count的值
- function WatchCount() {
- const [count, setCount] = useState(0);
- useEffect(function() {
- setInterval(function log() {
- console.log(`Count is: ${count}`);
- }, 2000);
- }, []);
- return (
- <div> {count} <button onClick={() => setCount(count + 1) }> Increase </button> </div>
- );
- }
打開事例(https://codesandbox.io/s/stale-closure-use-effect-broken-2-gyhzk)
并點(diǎn)擊幾次增加按鈕。然后看看控制臺,每2秒出現(xiàn)一次Count is: 0,盡管count狀態(tài)變量實(shí)際上已經(jīng)增加了幾次。
為什么會這樣?
第一次渲染時(shí),狀態(tài)變量count初始化為0。
組件安裝后,useEffect()調(diào)用 setInterval(log, 2000)計(jì)時(shí)器函數(shù),該計(jì)時(shí)器函數(shù)計(jì)劃每2秒調(diào)用一次log()函數(shù)。在這里,閉包log()捕獲到count變量為0。
之后,即使在單擊Increase按鈕時(shí)count增加,計(jì)時(shí)器函數(shù)每2秒調(diào)用一次的log(),使用count的值仍然是0。log()成為一個(gè)過時(shí)的閉包。
解決方案是讓useEffect()知道閉包log()依賴于count,并在count改變時(shí)正確處理間隔的重置
- function WatchCount() {
- const [count, setCount] = useState(0);
- useEffect(function() {
- const id = setInterval(function log() {
- console.log(`Count is: ${count}`);
- }, 2000);
- return function() {
- clearInterval(id);
- }
- }, [count]);
- return (
- <div>
- {count}
- <button onClick={() => setCount(count + 1) }>
- Increase
- </button>
- </div>
- );
- }
正確設(shè)置依賴項(xiàng)后,一旦count發(fā)生變化,useEffect()就會更新閉包。
3.2 useState()
組件有1個(gè)button ,以1秒延遲異步增加計(jì)數(shù)器。
- function DelayedCount() {
- const [count, setCount] = useState(0);
- function handleClickAsync() {
- setTimeout(function delay() {
- setCount(count + 1);
- }, 1000);
- }
- return (
- <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div>
- );
- }
現(xiàn)在打開演示(https://codesandbox.io/s/use-state-broken-0q994)??焖賳螕?次按鈕。計(jì)數(shù)器僅更新為1,而不是預(yù)期的2。
每次單擊setTimeout(delay, 1000)將在1秒后執(zhí)行delay()。delay()此時(shí)捕獲到的 count 為 0。
兩個(gè)delay()都將狀態(tài)更新為相同的值:setCount(count + 1) = setCount(0 + 1) = setCount(1)。
這是因?yàn)榈诙螁螕舻膁elay()閉包中已捕獲了過時(shí)的count變量為0。
為了解決這個(gè)問題,我們使用函數(shù)式方法setCount(count => count + 1)來更新count狀態(tài)
- function DelayedCount() {
- const [count, setCount] = useState(0);
- function handleClickAsync() {
- setTimeout(function delay() {
- setCount(count => count + 1); }, 1000);
- }
- function handleClickSync() {
- setCount(count + 1);
- }
- return (
- <div>
- {count}
- <button onClick={handleClickAsync}>Increase async</button>
- <button onClick={handleClickSync}>Increase sync</button>
- </div>
- );
- }
打開演示(https://codesandbox.io/s/use-state-fixed-zz78r)。再次快速單擊按鈕2次。計(jì)數(shù)器顯示正確的值2。
當(dāng)一個(gè)返回基于前一個(gè)狀態(tài)的新狀態(tài)的回調(diào)函數(shù)被提供給狀態(tài)更新函數(shù)時(shí),React確保將最新的狀態(tài)值作為該回調(diào)函數(shù)的參數(shù)提供
- setCount(alwaysActualStateValue => newStateValue);
這就是為什么在狀態(tài)更新過程中出現(xiàn)的過時(shí)裝飾問題可以通過函數(shù)這種方式來解決。
4.總結(jié)
當(dāng)閉包捕獲過時(shí)的變量時(shí),就會發(fā)生過時(shí)的閉包問題。
解決過時(shí)閉包的有效方法是正確設(shè)置React鉤子的依賴項(xiàng)?;蛘?,在失效狀態(tài)的情況下,使用函數(shù)方式更新狀態(tài)。
~完,我是小智,我要去刷碗了。
作者:Shadeed 譯者:前端小智 來源:dmitripavlutin原文:https://dmitripavlutin.com/react-hooks-stale-closures/
本文轉(zhuǎn)載自微信公眾號「大遷世界」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系大遷世界公眾號。