本文已經過原作者 Shadeed 授權翻譯。
Hooks 簡化了 React 元件內部狀態和副作用的管理。此外,可以將重複的邏輯提取到自定義 Hooks 中,以在整個應用程式中重複使用。
Hooks 嚴重依賴於 JS 閉包。這就是為什麼 Hooks 如此具有表現力和簡單,但是閉包有時很棘手。
使用 Hooks 時可能遇到的一個問題就是過時的閉包,這可能很難解決。
讓我們從過時的裝飾開始。然後,看看到過時的閉包如何影響 React Hooks,以及如何解決該問題。
1.過時的閉包工廠函式 createIncrement(incBy) 返回一個increment和log函式的元組。呼叫時,increment()函式將內部value增加incBy,而log()僅列印一條訊息,其中包含有關當前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(); // 1increment(); // 2increment(); // 3// 不能正確工作!log(); // "Current value is 0"
[increment, log] = createIncrement(1)返回一個函式元組:一個函式增加內部值,另一個函式記錄當前值。
然後,increment()的3次呼叫將 value遞增到3。
最後,log()呼叫列印訊息是 Current value is 0,這有點出乎意料的,因為此時 value 為 3 了。
log()是一個過時的閉包。閉包 log()捕獲了值為 "Current value is 0"的 message變數。
即使 value 變數在呼叫increment()時被增加多次,message變數也不會更新,並且總是保持一個過時的值 "Current value is 0"。
過時的閉包捕獲具有過時值的變數。
2.修復過時的閉包修復過時的log()問題需要關閉實際更改的變數:value的閉包。
我們將語句 const message = ...; 移動到 log() 函式內部:
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(); // 1increment(); // 2increment(); // 3// Works!log(); // "Current value is 3"
現在,在呼叫了 3 次 increment() 函式之後,呼叫 log() 記錄了實際value:"Current value is 3"。
3. Hooks 中的過時閉包3.1 useEffect()我們來看一下使用useEffect() 過時閉包的常見情況。
在元件<WatchCount>中,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)
為什麼會這樣?
第一次渲染時,狀態變數count初始化為0。
元件安裝後,useEffect()呼叫 setInterval(log, 2000)計時器函式,該計時器函式計劃每2秒呼叫一次log()函式。在這裡,閉包log()捕獲到count變數為0。
之後,即使在單擊Increase按鈕時count增加,計時器函式每2秒呼叫一次的log(),使用count的值仍然是0。log()成為一個過時的閉包。
解決方案是讓useEffect()知道閉包log()依賴於count,並在count改變時正確處理間隔的重置
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> );}
正確設定依賴項後,一旦count發生變化,useEffect()就會更新閉包。
3.2 useState()<DelayedCount>元件有1個button ,以1秒延遲非同步增加計數器。
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> );}
現在開啟演示(https://codesandbox.io/s/use-state-broken-0q994)。快速單擊2次按鈕。計數器僅更新為1,而不是預期的2。
每次單擊setTimeout(delay, 1000)將在1秒後執行delay()。delay()此時捕獲到的 count 為 0。
兩個delay()都將狀態更新為相同的值:setCount(count + 1) = setCount(0 + 1) = setCount(1)。
這是因為第二次單擊的delay()閉包中已捕獲了過時的count變數為0。
為了解決這個問題,我們使用函式式方法setCount(count => count + 1)來更新count狀態
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次。計數器顯示正確的值2。
當一個返回基於前一個狀態的新狀態的回撥函式被提供給狀態更新函式時,React確保將最新的狀態值作為該回調函式的引數提供
setCount(alwaysActualStateValue => newStateValue);
這就是為什麼在狀態更新過程中出現的過時裝飾問題可以透過函式這種方式來解決。
4.總結當閉包捕獲過時的變數時,就會發生過時的閉包問題。
解決過時閉包的有效方法是正確設定React鉤子的依賴項。或者,在失效狀態的情況下,使用函式方式更新狀態。
~完,我是小智,我要去刷碗了。
原文:https://dmitripavlutin.com/react-hooks-stale-closures/