使用 JS 及 React Hook 時需要注意過時閉包的坑


1. js 中的閉包

下面定義了一個工廠函數 createIncrement(i),它返回一個increment函數。之后,每次調用increment函數時,內部計數器的值都會增加i。

function createIncrement(i) { let value = 0; function increment() { value += i; console.log(value); } return increment; } const inc = createIncrement(1); inc(); // 1 inc(); // 2 

createIncrement(1) 返回一個增量函數,該函數賦值給inc變量。當調用inc()時,value 變量加1。

第一次調用inc()返回1,第二次調用返回2,依此類推。

這挺趣的,只要調用inc()還不帶參數,js 仍然知道當前 value 和 i 的增量,來看看這玩意是如何工作的。

原理就在 createIncrement() 中。當在函數上返回一個函數時,有會有閉包產生。閉包捕獲詞法作用域中的變量 value 和 i。

詞法作用域是定義閉包的外部作用域。在本例中,increment() 的詞法作用域是createIncrement()的作用域,其中包含變量 value 和 i。

clipboard.png

無論在何處調用 inc(),甚至在 createIncrement() 的作用域之外,它都可以訪問 value 和 i。

閉包是一個可以從其詞法作用域記住和修改變量的函數,不管執行作用域是什么。

繼續這個例子,可以在任何地方調用 inc(),甚至在異步回調中也可以:

(function() { inc(); // 3 }()); setTimeout(function() { inc(); // 4 }, 1000); 

 

2. react Hooks 中的閉包

通過簡化狀態重用和副作用管理,Hooks 取代了基於類的組件。此外,咱們可以將重復的邏輯提取到自定義 Hook 中,以便在應用程序之間重用。

Hooks 嚴重依賴於 JS 閉包,但是閉包有時很棘手。

當咱們使用一個有多種副作用和狀態管理的 react 組件時,可能會遇到的一個問題是過時的閉包,這可能很難解決。

咱們從提煉出過時的閉包開始。然后,看看過時的閉包如何影響 React Hook,以及如何解決這個問題。

 

3. 過時的閉包

工廠函數createIncrement(i)返回一個increment函數。increment 函數對 value 增加i請輸入代碼 ,並返回一個記錄當前 value 的函數

function createIncrement(i) { let value = 0; function increment() { value += i; console.log(value); const message = `Current value is ${value}`; return function logValue() { console.log(message); }; } return increment; } const inc = createIncrement(1); const log = inc(); // 打印 1 inc(); // 打印 2 inc(); // 打印 3 // 無法正確工作 log(); // 打印 "Current value is 1" 

在第一次調用inc()時,返回的閉包被分配給變量 log。對 inc() 的 3 次調用的增量 value 為 3。

最后,調用log() 打印 message “Current value is 1”,這是出乎意料的,因為此時 value 等於 3。

log()是過時的閉包。在第一次調用 inc() 時,閉包 log() 捕獲了具有 “Current value is 1” 的 message 變量。而現在,當 value 已經是 3 時,message 變量已經過時了。

過時的閉包捕獲具有過時值的變量。

 

4.修復過時閉包的問題

使用新的閉包

解決過時閉包的第一種方法是找到捕獲最新變量的閉包。

咱們找到捕獲了最新 message 變量的閉包。就是從最后一次調用 inc() 返回的閉包。

const inc = createIncrement(1); inc(); // 打印 1 inc(); // 打印 2 const latestLog = inc(); // 打印 3 // 正常工作 latestLog(); // 打印 "Current value is 3" 

latestLog 捕獲的 message 變量具有最新的的值 “Current value is 3”。

順便說一下,這大概就是 React Hook 處理閉包新鮮度的方式。

Hooks 實現假設在組件重新渲染之間,作為 Hook 回調提供的最新閉包(例如 useEffect(callback)) 已經從組件的函數作用域捕獲了最新的變量。

關閉已更改的變量

第二種方法是讓logValue()直接使用 value。

讓我們移動行 const message = ...; 到 logValue() 函數體中:

function createIncrementFixed(i) { let value = 0; function increment() { value += i; console.log(value); return function logValue() { const message = `Current value is ${value}`; console.log(message); }; } return increment; } const inc = createIncrementFixed(1); const log = inc(); // 打印 1 inc(); // 打印 2 inc(); // 打印 3 // 正常工作 log(); // 打印 "Current value is 3" 

logValue() 關閉 createIncrementFixed() 作用域內的 value 變量。log() 現在打印正確的消息“Current value is 3”。

PPT模板下載大全https://www.wode007.com

5. Hook 中過時的閉包

useEffect()

現在來研究一下在使用 useEffect() Hook 時出現過時閉包的常見情況。

在組件 <WatchCount> 中,useEffect()每秒打印 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) }> 加1 </button> </div> ); } 

打開 CodeSandbox 並單擊幾次加1按鈕。然后看看控制台,每2秒打印 Count is: 0。

咋這樣呢?

在第一次渲染時,log() 中閉包捕獲 count 變量的值 0。過后,即使 count 增加,log()中使用的仍然是初始化的值 0。log() 中的閉包是一個過時的閉包。

解決方案是讓 useEffect()知道 log() 中的閉包依賴於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() 就更新閉包。

同樣打開修復的 codesandbox,單擊幾次加1按鈕。然后看看控制台,這次打印就是正確的值了。

正確管理 Hook 依賴關系是解決過時閉包問題的關鍵。推薦安裝 eslint-plugin-react-hooks,它可以幫助咱們檢測被遺忘的依賴項。

useState()

組件<DelayedCount>有 2 個按鈕:

點擊按鍵 “Increase async” 在異步模式下以1秒的延遲遞增計數器,在同步模式下,點擊按鍵 “Increase sync” 會立即增加計數器。

function DelayedCount() { const [count, setCount] = useState(0); function handleClickAsync() { setTimeout(function delay() { setCount(count + 1); }, 1000); } function handleClickSync() { setCount(count + 1); } return ( <div> {count} <button onClick={handleClickAsync}>Increase async</button> <button onClick={handleClickSync}>Increase sync</button> </div> ); }

現在打開 codesandbox 演示。點擊 “Increase async” 按鍵然后立即點擊 “Increase sync” 按鈕,count 只更新到 1。

這是因為 delay() 是一個過時的閉包。

來看看這個過程發生了什么:

  1. 初始渲染:count 值為 0。
  2. 點擊 'Increase async' 按鈕。delay() 閉包捕獲 count 的值 0。setTimeout() 1 秒后調用 delay()。
  3. 點擊 “Increase async” 按鍵。handleClickSync() 調用 setCount(0 + 1) 將 count 的值設置為 1,組件重新渲染。
  4. 1 秒之后,setTimeout() 執行 delay() 函數。但是 delay() 中閉包保存 count 的值是初始渲染的值 0,所以調用 setState(0 + 1),結果count保持為 1。

delay() 是一個過時的閉包,它使用在初始渲染期間捕獲的過時的 count 變量。

為了解決這個問題,可以使用函數方法來更新 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> ); } 

現在 setCount(count => count + 1) 更新了 delay() 中的 count 狀態。React 確保將最新狀態值作為參數提供給更新狀態函數,過時的閉包的問題就解決了。

 

總結

閉包是一個函數,它從定義變量的地方(或其詞法范圍)捕獲變量。閉包是每個 JS 開發人員都應該知道的一個重要概念。

當閉包捕獲過時的變量時,就會出現過時閉包的問題。解決過時閉包的一個有效方法是正確設置 React Hook 的依賴項。或者,對於過時的狀態,使用函數方式更新狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM