今天中午在領完盒飯,吃飯的時候,正吃着深海鱈魚片,蘸上番茄醬,那美味,簡直無以言表。忽然產品急匆匆的跑過來講:「今天需求能上線吧?」我突然虎軀一震,想到本身遇到個問題遲遲找不到緣由,怯怯的回答道:「能...能吧...」,產品聽到‘能’這個字便哼着小曲揚長而去,留下我獨自一人,面對着已經變味的深海鱈魚片...一遍又一遍的想着問題該如何解決...react
JS
的閉包本質上源自兩點,詞法做用域和函數當前值傳遞。npm
閉包的造成很簡單,就是在函數執行完畢後,返回函數,或者將函數得以保留下來,即造成閉包。bash
關於詞法做用域
相關的知識點,能夠查閱《你不知道的JavaScript》
找到答案。閉包
React Hooks
中的閉包和咱們在JS
中見到的閉包並沒有不一樣。異步
定義一個工廠函數createIncrement(i)
,返回一個increment
函數。每次調用increment
函數時,內部計數器的值都會增長i
。async
function createIncrement(i) {
let value = 0
function increment() {
value += i
console.log(value)
}
return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20
複製代碼
createIncrement(10)
返回一個增量函數,該函數賦值給inc
變量。當調用inc()
時,value
變量加10。函數
第一次調用inc()
返回10,第二次調用返回20,依此類推。ui
調用inc()
時不帶參數,JS
仍然能夠獲取到當前 value
和 i
的增量,來看看它是如何工做的。spa
原理就在 createIncrement()
中。當在函數上返回一個函數時,就會有閉包產生。閉包捕獲了詞法做用域中的變量 value
和 i
。eslint
詞法做用域是定義閉包的外部做用域
。在本例中,increment()
的詞法做用域是createIncrement()
的做用域,其中包含變量 value
和 i
。
不管在何處調用 inc()
,甚至在 createIncrement()
的做用域以外,它均可以訪問 value
和 i
。
閉包是一個能夠從其詞法做用域
記住和修改變量的函數,無論執行的做用域是什麼。
經過簡化狀態重用和反作用管理,Hooks
取代了基於類的組件。此外,我們能夠將重複的邏輯提取到自定義 Hook
中,以便在應用程序之間重用。Hooks
嚴重依賴於 JS
閉包,可是閉包有時很棘手。
當我們使用一個有多種反作用和狀態管理的 React
組件時,可能會遇到的一個問題是過期的閉包,這可能很難解決。
工廠函數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() { // setState至關於logValue函數
console.log(message)
}
}
return increment
}
const inc = createIncrement(10)
const log = inc() // 10,將當前的value值固定
inc() // 20
inc() // 30
log() // "Current value is 10" 未能正確打印30
複製代碼
function createIncrement(i) {
let value = 0
function increment() {
value += i
console.log(value)
const message = `Current value is ${value}`
return function logValue() { // setState至關於logValue函數
console.log(message)
}
}
return increment
}
const inc = createIncrement(1) // i被固定爲1,輸入幾就被固定爲幾
inc() // 1
const log = inc() // 2
inc() // 3
log() // "Current value is 2" 未能正確打印3
複製代碼
過期的閉包捕獲具備過期值的變量。
解決過期閉包的第一種方法是找到捕獲最新變量的閉包。
找到捕獲了最新message
變量的閉包,就是從最後一次調用inc()
返回的閉包。
const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"
複製代碼
以上就是React Hook
處理閉包新鮮度的方法了。
Hooks
實現假設在組件從新渲染以前,最爲Hook
回調提供的最新閉包(例如useEffect(callback))
已經從組件的函數做用域捕獲了最新的變量。也就是說在useEffect
的第二個參數[]
加入監聽變化的值,在每次變化時,執行function
,獲取最新的閉包。
第二種方法是讓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()
如今打印正確的消息。
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>
)
}
複製代碼
點擊幾回加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]); // 看這裏,這行是重點,count變化後從新渲染useEffect
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
複製代碼
設置依賴項後,一旦count
更改,useEffect()
就更新閉包。
正確管理 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>
)
}
複製代碼
點擊 「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
確保將最新狀態值做爲參數提供給更新狀態函數,過期的閉包的問題就解決了。
閉包是一個函數,它從定義變量的地方(或其詞法範圍)捕獲變量。
當閉包捕獲過期的變量
時,就會出現過期閉包的問題。
React Hook
的依賴項