做者:Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin
上個月本身花了 1300 買了阿里的服務器來學習 node 及對應的框架,在 11 號以前它們有作活動,1300 的配置如今一年只要 86 元,三年只要229元,真心以爲很划算了,能夠點擊下面連接進行參與:javascript
https://www.aliyun.com/1111/2...html
爲了保證的可讀性,本文采用意譯而非直譯。前端
下面定義了一個工廠函數 createIncrement(i)
,它返回一個increment
函數。以後,每次調用increment
函數時,內部計數器的值都會增長i
。java
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
。node
第一次調用inc()
返回1
,第二次調用返回2
,依此類推。react
這挺趣的,只要調用inc()
還不帶參數,JS 仍然知道當前 value
和 i
的增量,來看看這玩意是如何工做的。git
原理就在 createIncrement()
中。當在函數上返回一個函數時,有會有閉包產生。閉包捕獲詞法做用域中的變量 value
和 i
。github
詞法做用域是定義閉包的外部做用域。在本例中,increment()
的詞法做用域是createIncrement()
的做用域,其中包含變量 value
和 i
。npm
不管在何處調用 inc()
,甚至在 createIncrement()
的做用域以外,它均可以訪問 value
和 i
。segmentfault
閉包是一個能夠從其詞法做用域記住和修改變量的函數,無論執行做用域是什麼。
繼續這個例子,能夠在任何地方調用 inc()
,甚至在異步回調中也能夠:
(function() { inc(); // 3 }()); setTimeout(function() { inc(); // 4 }, 1000);
經過簡化狀態重用和反作用管理,Hooks 取代了基於類的組件。此外,我們能夠將重複的邏輯提取到自定義 Hook 中,以便在應用程序之間重用。
Hooks 嚴重依賴於 JS 閉包,可是閉包有時很棘手。
當我們使用一個有多種反作用和狀態管理的 React 組件時,可能會遇到的一個問題是過期的閉包,這可能很難解決。
我們從提煉出過期的閉包開始。而後,看看過期的閉包如何影響 React Hook,以及如何解決這個問題。
工廠函數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
變量已通過時了。
過期的閉包捕獲具備過期值的變量。
解決過期閉包的第一種方法是找到捕獲最新變量的閉包。
我們找到捕獲了最新 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
」。
如今來研究一下在使用 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,它能夠幫助我們檢測被遺忘的依賴項。
組件<DelayedCount>
有 2 個按鈕:
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
。delay()
閉包捕獲 count
的值 0
。setTimeout()
1 秒後調用 delay()
。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 的依賴項。或者,對於過期的狀態,使用函數方式更新狀態。
你認爲閉包使得 React Hook 很難理解嗎?
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
原文:
https://dmitripavlutin.com/si...
https://dmitripavlutin.com/re...
阿里雲最近在作活動,低至2折,有興趣能夠看看:https://promotion.aliyun.com/...
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
https://github.com/qq449245884/xiaozhi
由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。
每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵