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。

無論在何處調用 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() 是一個過時的閉包。
來看看這個過程發生了什么:
- 初始渲染:count 值為 0。
- 點擊 'Increase async' 按鈕。delay() 閉包捕獲 count 的值 0。setTimeout() 1 秒后調用 delay()。
- 點擊 “Increase async” 按鍵。handleClickSync() 調用 setCount(0 + 1) 將 count 的值設置為 1,組件重新渲染。
- 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 的依賴項。或者,對於過時的狀態,使用函數方式更新狀態。
