做者:Dmitri Pavlutinjavascript
譯者:前端小智html
來源:dmitripavlutin前端
爲了保證的可讀性,本文采用意譯而非直譯。node
下面定義了一個工廠函數 createIncrement(i)
,它返回一個increment
函數。以後,每次調用increment
函數時,內部計數器的值都會增長i
。react
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
。git
第一次調用inc()
返回1
,第二次調用返回2
,依此類推。github
這挺趣的,只要調用inc()
還不帶參數,JS 仍然知道當前 value
和 i
的增量,來看看這玩意是如何工做的。npm
原理就在 createIncrement()
中。當在函數上返回一個函數時,有會有閉包產生。閉包捕獲詞法做用域中的變量 value
和 i
。微信
詞法做用域是定義閉包的外部做用域。在本例中,increment()
的詞法做用域是createIncrement()
的做用域,其中包含變量 value
和 i
。
不管在何處調用 inc()
,甚至在 createIncrement()
的做用域以外,它均可以訪問 value
和 i
。
閉包是一個能夠從其詞法做用域記住和修改變量的函數,無論執行做用域是什麼。
繼續這個例子,能夠在任何地方調用 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 個按鈕:
點擊按鍵 「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 的依賴項。或者,對於過期的狀態,使用函數方式更新狀態。
你認爲閉包使得 React Hook 很難理解嗎?
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
原文: dmitripavlutin.com/simple-expl… dmitripavlutin.com/react-hooks…
阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。
每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵