詳解如何在React中逃離閉包陷阱
眾所周知,JavaScript 中的閉包(Closures)一定是這種語(yǔ)言最可怕的特性之一,即使是無(wú)所不知的 ChatGPT 也是這樣說(shuō)的。另外它可能也是最隱蔽的語(yǔ)言特性之一,我們?cè)诰帉?React 代碼時(shí)經(jīng)常會(huì)用到它,但是大多數(shù)時(shí)候我們甚至沒(méi)有意識(shí)到這一點(diǎn)。但是,我們終究還是離不開(kāi)它:如果我們想編寫復(fù)雜且性能很好的 React 應(yīng)用,就必須了解閉包。所以,今天我們一起來(lái)學(xué)習(xí)以下幾點(diǎn):
- 什么是閉包,它們是如何出現(xiàn)的,為什么我們需要它們。
- 什么是過(guò)期的閉包,它們?yōu)槭裁磿?huì)出現(xiàn)。
- React 中導(dǎo)致過(guò)期閉包的常見(jiàn)場(chǎng)景是什么,以及如何應(yīng)對(duì)它們。
警告:如果你從未接觸過(guò) React 中的閉包,本文可能會(huì)讓你腦漿迸裂,在閱讀本文時(shí),請(qǐng)確保隨身攜帶足夠的巧克力來(lái)刺激你的腦細(xì)胞。
一個(gè)常見(jiàn)的問(wèn)題
比如現(xiàn)在有這樣一個(gè)場(chǎng)景:你正在實(shí)現(xiàn)一個(gè)帶有幾個(gè)輸入字段的表單。其中一個(gè)字段是來(lái)自某個(gè)外部的組件庫(kù)。你無(wú)法訪問(wèn)它的內(nèi)部結(jié)構(gòu),所以也沒(méi)辦法解決它的性能問(wèn)題。但你確實(shí)需要在表單中使用它,因此你決定用 React.memo
封裝它,以便在表單中的狀態(tài)發(fā)生變化時(shí)盡量減少它的重新渲染。類似這樣:
const HeavyComponentMemo = React.memo(HeavyComponent); const Form = () => { const [value, setValue] = useState(); return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo /> </> ); };
這個(gè) Heavy 組件只接受一個(gè)字符串 props(比如 title)和一個(gè) onClick 回調(diào)。當(dāng)你點(diǎn)擊該組件中的 "完成" 按鈕時(shí),就會(huì)觸發(fā)這個(gè)回調(diào)。如果你想在點(diǎn)擊時(shí)提交表單數(shù)據(jù)。這也很簡(jiǎn)單:只需將 title 和 onClick 這兩個(gè) props 傳遞給它即可。
const HeavyComponentMemo = React.memo(HeavyComponent); const Form = () => { const [value, setValue] = useState(); const onClick = () => { // submit our form data here console.log(value); }; return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo title="Welcome to the form" onClick={onClick} /> </> ); };
現(xiàn)在,你又會(huì)面臨一個(gè)新的問(wèn)題。我們知道,React.memo 封裝的組件上的每個(gè) props 都必須是原始值,或者在重新渲染時(shí)是保持不變的。否則,memoization 就是不起作用的。所以,從技術(shù)上講,我們需要將 onClick 包裝為 useCallback:
const onClick = useCallback(() => { // submit data here }, []);
但我們也知道,useCallback 鉤子應(yīng)在其依賴關(guān)系數(shù)組中聲明所有依賴關(guān)系。因此,如果我們想在其中提交表單數(shù)據(jù),就必須將該數(shù)據(jù)聲明為依賴項(xiàng):
const onClick = useCallback(() => { // submit data here console.log(value); // adding value to the dependency }, [value]);
現(xiàn)在的難題是:即使我們的 onClick 被 memo 化了,但每次表單有重新輸入時(shí),它仍然會(huì)發(fā)生變化。因此,我們的性能優(yōu)化毫無(wú)用處。
下面讓我們尋找一下其他的解決方案。React.memo
有一個(gè)叫做比較函數(shù)的東西,它允許我們對(duì) React.memo
中的 props 比較進(jìn)行更精細(xì)的控制。通常,React 會(huì)自行比較前后的 props 。如果我們提供這個(gè)函數(shù),它將依賴于其返回的結(jié)果。如果返回結(jié)果為 true,那么 React 就會(huì)知道 props 是相同的,組件就不應(yīng)該被重新渲染,聽(tīng)起來(lái)正是我們需要的。我們只需要更新一個(gè) props ,那就是我們的 title ,所以不會(huì)很復(fù)雜:
const HeavyComponentMemo = React.memo( HeavyComponent, (before, after) => { return before.title === after.title; }, );
這樣,完整的代碼就是這樣的:
const HeavyComponentMemo = React.memo( HeavyComponent, (before, after) => { return before.title === after.title; }, ); const Form = () => { const [value, setValue] = useState(); const onClick = () => { // submit our form data here console.log(value); }; return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo title="Welcome to the form" onClick={onClick} /> </> ); };
起作用了,我們?cè)谳斎肟蛑休斎雰?nèi)容,Heavy 組件不會(huì)重新渲染,性能也不會(huì)受到影響。
但是我們又遇到了新的問(wèn)題:如果在輸入框中輸入內(nèi)容,然后按下按鈕,我們?cè)?onClick 中打印的值是 undefined 。但它不可能是 undefined,如果我在 onClick 之外添加 console.log
,它就會(huì)正確打印。
// those one logs it correctly console.log(value); const onClick = () => { // this is always undefined console.log(value); };
這是怎么回事呢?
這就是所謂的 "過(guò)期閉包" 問(wèn)題。為了解決這個(gè)問(wèn)題,我們首先需要了解一下 JavaScript 中最令人恐懼的話題:閉包及其工作原理。
JavaScript、作用域和閉包
讓我們從函數(shù)和變量開(kāi)始,當(dāng)我們?cè)?JavaScript 中聲明一個(gè)普通函數(shù)或者尖頭函數(shù)會(huì)發(fā)生什么呢?
function something() { // } const something = () => {};
通過(guò)這樣的操作,我們創(chuàng)建了一個(gè)局部作用域:代碼中的一個(gè)區(qū)域,其中聲明的變量從外部是不可見(jiàn)的。
const something = () => { const value = 'text'; }; console.log(value); // not going to work, "value" is local to "something" function
每次我們創(chuàng)建函數(shù)時(shí)都會(huì)發(fā)生這種情況。在另一個(gè)函數(shù)內(nèi)部創(chuàng)建的函數(shù)將具有自己的局部作用域,對(duì)于外部函數(shù)不可見(jiàn)。
const something = () => { const inside = () => { const value = 'text'; }; console.log(value); // not going to work, "value" is local to "inside" function };
然而,在相反的方向就不一樣了,最里面的函數(shù)可以訪問(wèn)到外部聲明的所有變量。
const something = () => { const value = 'text'; const inside = () => { // perfectly fine, value is available here console.log(value); }; };
這就是通過(guò)創(chuàng)建所謂的 “閉包” 來(lái)實(shí)現(xiàn)的。內(nèi)部函數(shù) “閉包” 了來(lái)自外部的所有數(shù)據(jù),它本質(zhì)上就是所有 “外部” 數(shù)據(jù)的快照,這些數(shù)據(jù)被凍結(jié)并單獨(dú)存儲(chǔ)在內(nèi)存中。如果我們不是在 something 函數(shù)內(nèi)創(chuàng)建該值,而是將其作為參數(shù)傳遞并返回內(nèi)部函數(shù)呢:
const something = (value) => { const inside = () => { // perfectly fine, value is available here console.log(value); }; return inside; };
我們會(huì)得到這樣的行為:
const first = something('first'); const second = something('second'); first(); // logs "first" second(); // logs "second"
我們調(diào)用 something 函數(shù)時(shí)傳入值 first,并將結(jié)果分配給一個(gè)變量。結(jié)果是對(duì)內(nèi)部聲明的函數(shù)的引用,形成閉包。從現(xiàn)在開(kāi)始,只要保存這個(gè)引用的第一個(gè)變量是存在的,我們傳遞給它的值 “first” 就會(huì)被凍結(jié)掉,并且內(nèi)部函數(shù)將可以訪問(wèn)它。
第二次調(diào)用也是同樣的情況:我們傳遞了一個(gè)不同的值,形成一個(gè)閉包,返回的函數(shù)也將永遠(yuǎn)可以訪問(wèn)該變量。
在 something 函數(shù)中本地聲明的任何變量都是如此:
const something = (value) => { const r = Math.random(); const inside = () => { // ... }; return inside; }; const first = something('first'); const second = something('second'); first(); // logs random number second(); // logs another random number
這就像拍攝一些動(dòng)態(tài)場(chǎng)景的照片一樣:只要按下按鈕,整個(gè)場(chǎng)景就會(huì)永遠(yuǎn) “凍結(jié)” 在照片中。下次按下按鈕不會(huì)改變之前拍攝的照片中的任何內(nèi)容。
在 React 中,我們一直都在創(chuàng)建閉包,甚至沒(méi)有意識(shí)到,組件內(nèi)聲明的每個(gè)回調(diào)函數(shù)都是一個(gè)閉包:
const Component = () => { const onClick = () => { // closure! }; return <button onClick={onClick} />; };
useEffect 或 useCallback 鉤子中的所有內(nèi)容都是一個(gè)閉包:
const Component = () => { const onClick = useCallback(() => { // closure! }); useEffect(() => { // closure! }); };
它們都可以訪問(wèn)組件中聲明的 state、props 和局部變量:
const Component = () => { const [state, setState] = useState(); const onClick = useCallback(() => { // perfectly fine console.log(state); }); useEffect(() => { // perfectly fine console.log(state); }); };
組件內(nèi)的每個(gè)函數(shù)都是一個(gè)閉包,因?yàn)榻M件本身只是一個(gè)函數(shù)。
過(guò)期閉包的問(wèn)題
但是,以上所有的內(nèi)容,如果你之前沒(méi)有接觸過(guò)閉包的話會(huì)覺(jué)得挺新奇的,但其實(shí)還是挺簡(jiǎn)單的,你多創(chuàng)建幾個(gè)函數(shù),就會(huì)變得很自然了。我們寫了這么久的 React 甚至也不需要理解 “閉包” 的概念。
那么問(wèn)題出在哪里呢?為什么閉包是 JavaScript 中最可怕的東西之一,并讓如此多的開(kāi)發(fā)者感到痛苦?
因?yàn)橹灰痖]包的函數(shù)存在引用,閉包就會(huì)一直存在。而函數(shù)的引用只是一個(gè)值,可以賦給任何東西。
比如這個(gè)函數(shù),它返回一個(gè)完全無(wú)辜的閉包:
const something = (value) => { const inside = () => { console.log(value); }; return inside; };
問(wèn)題是每次調(diào)用都會(huì)重新創(chuàng)建內(nèi)部函數(shù),如果我決定嘗試緩存它,會(huì)發(fā)生什么情況呢?類似這樣:
const cache = {}; const something = (value) => { if (!cache.current) { cache.current = () => { console.log(value); }; } return cache.current; };
從表面上看,這段代碼并沒(méi)有什么問(wèn)題。我們只是創(chuàng)建了一個(gè)名為 cache 的外部變量,并將內(nèi)部函數(shù)分配給 cache.current 屬性。然后,我們就不會(huì)再每次都重新創(chuàng)建這個(gè)函數(shù)了,而是直接返回已經(jīng)保存的值。
但是,如果我們嘗試多調(diào)用幾次,就會(huì)發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象:
const first = something('first'); const second = something('second'); const third = something('third'); first(); // logs "first" second(); // logs "first" third(); // logs "first"
無(wú)論我們用不同的參數(shù)調(diào)用多少次 something 函數(shù),記錄的值始終是第一個(gè)參數(shù)!
我們剛剛就創(chuàng)建了一個(gè)所謂的 "過(guò)期閉包"。每個(gè)閉包在創(chuàng)建時(shí)都是凍結(jié)的,當(dāng)我們第一次調(diào)用 something 函數(shù)時(shí),我們創(chuàng)建了一個(gè)值變量中包含 "first" 的閉包。然后,我們把它保存在 something 函數(shù)之外的一個(gè)對(duì)象中。
當(dāng)我們下一次調(diào)用 something 函數(shù)時(shí),我們將返回之前創(chuàng)建的閉包,而不是創(chuàng)建一個(gè)帶有新閉包的新函數(shù)。這個(gè)閉包會(huì)與 "first" 變量永遠(yuǎn)凍結(jié)在一起。
為了修復(fù)這種問(wèn)題,我們可以在每次值發(fā)生變化時(shí)重新創(chuàng)建函數(shù)及其閉包,類似這樣:
const cache = {}; let prevValue; const something = (value) => { // check whether the value has changed if (!cache.current || value !== prevValue) { cache.current = () => { console.log(value); }; } // refresh it prevValue = value; return cache.current; };
將值保存在變量中,以便我們可以將下一個(gè)值與前一個(gè)值進(jìn)行比較。如果變量發(fā)生了變化,則刷新 cache.current 閉包?,F(xiàn)在,它就會(huì)正確打印變量,如果我們比較具有相同值的函數(shù),比較結(jié)果將返回 true:
const first = something('first'); const anotherFirst = something('first'); const second = something('second'); first(); // logs "first" second(); // logs "second" console.log(first === anotherFirst); // will be true
React 中的過(guò)期閉包:useCallback
我們剛剛實(shí)現(xiàn)了與 useCallback 鉤子幾乎一模一樣的功能!每次使用 useCallback 時(shí),我們都會(huì)創(chuàng)建一個(gè)閉包,并緩存?zhèn)鬟f給它的函數(shù):
// that inline function is cached exactly as in the section before const onClick = useCallback(() => { }, []);
如果我們需要訪問(wèn)此函數(shù)內(nèi)的 state 或 props,我們需要將它們添加到依賴項(xiàng)數(shù)組中:
const Component = () => { const [state, setState] = useState(); const onClick = useCallback(() => { // access to state inside console.log(state); // need to add this to the dependencies array }, [state]); };
這個(gè)依賴關(guān)系數(shù)組會(huì)讓 React 刷新緩存的閉包,就像我們?cè)诒容^ value !== prevValue
時(shí)所做的一樣。如果我忘記了這個(gè)數(shù)組,我們的閉包就會(huì)過(guò)期:
const Component = () => { const [state, setState] = useState(); const onClick = useCallback(() => { // state will always be the initial state value here // the closure is never refreshed console.log(state); // forgot about dependencies }, []); };
每次我們觸發(fā)該回調(diào)時(shí),所有將被打印的內(nèi)容都是 undefined。
React 中的過(guò)期閉包:Refs
在 useCallback 和 useMemo 鉤子之后,引入過(guò)期閉包問(wèn)題的第二個(gè)最常見(jiàn)的方法是 Refs。
如果我嘗試對(duì) onClick 回調(diào)使用 Ref 而不是 useCallback 鉤子,會(huì)發(fā)生什么情況呢?有些文章會(huì)建議通過(guò)這樣做來(lái) memoize 組件上的 props。從表面上看,它確實(shí)看起來(lái)更簡(jiǎn)單:只需將一個(gè)函數(shù)傳遞給 useRef 并通過(guò) ref.current 訪問(wèn)它,沒(méi)有依賴性,不用擔(dān)心。
const Component = () => { const ref = useRef(() => { // click handler }); // ref.current stores the function and is stable between re-renders return <HeavyComponent onClick={ref.current} />; };
然而,組件內(nèi)的每個(gè)函數(shù)都會(huì)形成一個(gè)閉包,包括我們傳遞給 useRef 的函數(shù)。我們的 ref 在創(chuàng)建時(shí)只會(huì)初始化一次,并且不會(huì)自行更新。這基本上就是我們一開(kāi)始創(chuàng)建的邏輯,只是我們傳遞的不是值,而是我們想要保留的函數(shù)。像這樣:
const ref = {}; const useRef = (callback) => { if (!ref.current) { ref.current = callback; } return ref.current; };
因此,在這種情況下,一開(kāi)始(即組件剛剛初始化時(shí))形成的閉包將會(huì)被保留,永遠(yuǎn)不會(huì)刷新。當(dāng)我們?cè)噲D訪問(wèn)存儲(chǔ)在 Ref 中的函數(shù)內(nèi)部的 state 或 props 時(shí),我們只能得到它們的初始值:
const Component = ({ someProp }) => { const [state, setState] = useState(); const ref = useRef(() => { // both of them will be stale and will never change console.log(someProp); console.log(state); }); };
為了解決這個(gè)問(wèn)題,我們需要確保每次我們?cè)噲D訪問(wèn)的內(nèi)容發(fā)生變化時(shí),ref 值都會(huì)更新。本質(zhì)上,我們需要實(shí)現(xiàn) useCallback 鉤子的依賴數(shù)組所做的事情。
const Component = ({ someProp }) => { // initialize ref - creates closure! const ref = useRef(() => { // both of them will be stale and will never change console.log(someProp); console.log(state); }); useEffect(() => { // update the closure when state or props change ref.current = () => { console.log(someProp); console.log(state); }; }, [state, someProp]); };
React 中的過(guò)期閉包:React.memo
最后,我們回到文章的開(kāi)頭,回到引發(fā)這一切的謎團(tuán)。讓我們?cè)賮?lái)看看有問(wèn)題的代碼:
const HeavyComponentMemo = React.memo( HeavyComponent, (before, after) => { return before.title === after.title; }, ); const Form = () => { const [value, setValue] = useState(); const onClick = () => { // submit our form data here console.log(value); }; return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo title="Welcome to the form" onClick={onClick} /> </> ); };
每次點(diǎn)擊按鈕時(shí),都會(huì)打印 "undefined" 。我們?cè)?onClick 中的值從未更新過(guò),你能告訴我為什么嗎?
當(dāng)然,這又是一個(gè)過(guò)期閉包。當(dāng)我們創(chuàng)建 onClick 時(shí),首先使用默認(rèn)狀態(tài)值(undefined)形成閉包。我們將該閉包與 title 屬性一起傳遞給我們的 Memo 組件。在比較函數(shù)中,我們只比較了標(biāo)題。它永遠(yuǎn)不會(huì)改變,它只是一個(gè)字符串。比較函數(shù)始終返回 true,HeavyComponent 永遠(yuǎn)不會(huì)更新,因此,它保存的是對(duì)第一個(gè) onClick 閉包的引用,并具有凍結(jié)的 undefined 值。
既然我們知道了問(wèn)題所在,那么該如何解決呢?說(shuō)起來(lái)容易做起來(lái)難...
理想情況下,我們應(yīng)該在比較函數(shù)中對(duì)每個(gè) props 進(jìn)行比較,因此我們需要在其中加入 onClick:
(before, after) => { return ( before.title === after.title && before.onClick === after.onClick ); };
不過(guò),在這種情況下,這意味著我們只是重新實(shí)現(xiàn)了 React 的默認(rèn)行為,做的事情與不帶比較函數(shù)的 React.memo 完全一樣。因此,我們可以放棄它,只保留 React.memo (HeavyComponent)。
但這樣做意味著我們需要將 onClick 包裝為 useCallback。但這取決于 state ,我們又回到了原點(diǎn):每次狀態(tài)改變時(shí),我們的 HeavyComponent 都會(huì)重新渲染,這正是我們想要避免的。
我們還可以嘗試很多其他方法,但我們不必進(jìn)行任何大量的重構(gòu)就能擺脫閉包陷阱,有一個(gè)很酷的技巧可以幫助我們。
使用 Refs 逃離閉包陷阱
讓我們暫時(shí)擺脫 React.memo
和 onClick 實(shí)現(xiàn)中的比較函數(shù)。只需一個(gè)具有 state 和 memo 化 HeavyComponent 的 pure component 即可:
const HeavyComponentMemo = React.memo(HeavyComponent); const Form = () => { const [value, setValue] = useState(); return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo title="Welcome to the form" onClick={...} /> </> ); }
現(xiàn)在我們需要添加一個(gè) onClick 函數(shù),該函數(shù)在重新渲染的時(shí)候會(huì)保持穩(wěn)定,但也可以訪問(wèn)最新?tīng)顟B(tài)而無(wú)需重新創(chuàng)建。我們將把它存儲(chǔ)在 Ref 中,所以我們暫時(shí)添加一個(gè)空的:
const Form = () => { const [value, setValue] = useState(); // adding an empty ref const ref = useRef(); };
為了讓函數(shù)能夠訪問(wèn)最新?tīng)顟B(tài),每次重新渲染時(shí)都需要重新創(chuàng)建函數(shù),這是無(wú)法避免的,這也是閉包的本質(zhì),與 React 無(wú)關(guān)。我們應(yīng)該在 useEffect 中修改 Ref,而不是直接在渲染中修改 Ref,所以我們可以這樣做:
const Form = () => { const [value, setValue] = useState(); // adding an empty ref const ref = useRef(); useEffect(() => { // our callback that we want to trigger // with state ref.current = () => { console.log(value); }; // no dependencies array! }); };
不帶依賴數(shù)組的 useEffect 會(huì)在每次重新渲染時(shí)觸發(fā)。這正是我們想要的,所以現(xiàn)在在我們的 ref.current 中,我們有一個(gè)每次重新渲染都會(huì)重新創(chuàng)建的閉包,因此打印的 state 始終是最新的。
但我們不能把 ref.current
直接傳遞給 memoized 組件。每次重新渲染時(shí),這個(gè)值都會(huì)不同, memoization 將無(wú)法工作。
const Form = () => { const ref = useRef(); useEffect(() => { ref.current = () => { console.log(value); }; }); return ( <> {/* Can't do that, will break memoization */} <HeavyComponentMemo onClick={ref.current} /> </> ); };
所以,我們創(chuàng)建一個(gè)封裝在 useCallback 中的空函數(shù),并且不依賴于此函數(shù)。
const Form = () => { const ref = useRef(); useEffect(() => { ref.current = () => { console.log(value); }; }); const onClick = useCallback(() => { // empty dependency! will never change }, []); return ( <> {/* Now memoization will work, onClick never changes */} <HeavyComponentMemo onClick={onClick} /> </> ); };
現(xiàn)在,memoization 可以完美地工作,因?yàn)?onClick 從未改變。但有一個(gè)問(wèn)題:它什么也會(huì)不做。
這里有一個(gè)神奇的竅門:我們只需在 memoized 回調(diào)中調(diào)用 ref.current 即可:
useEffect(() => { ref.current = () => { console.log(value); }; }); const onClick = useCallback(() => { // call the ref here ref.current(); // still empty dependencies array! }, []);
注意到 ref 并不在 useCallback 的依賴關(guān)系中嗎?ref 本身是不會(huì)改變的。它只是 useRef 鉤子返回的一個(gè)可變對(duì)象的引用。但是,當(dāng)閉包凍結(jié)周圍的一切時(shí),并不會(huì)使對(duì)象不可變或被凍結(jié)。對(duì)象存儲(chǔ)在內(nèi)存的不同部分,多個(gè)變量可以包含對(duì)完全相同對(duì)象的引用。
const a = { value: 'one' }; // b is a different variable that references the same object const b = a;
如果我通過(guò)其中一個(gè)引用更改對(duì)象,然后通過(guò)另一個(gè)引用訪問(wèn)它,更改就會(huì)出現(xiàn):
a.value = 'ConardLi'; console.log(b.value); // will be "ConardLi"
在我們的案例中,這種情況并沒(méi)有發(fā)生:我們?cè)?useCallback 和 useEffect 中擁有完全相同的引用。因此,當(dāng)我們更改 useEffect 中 ref 對(duì)象的 current 屬性時(shí),我們可以在 useCallback 中訪問(wèn)該屬性,這個(gè)屬性恰好是一個(gè)捕獲了最新?tīng)顟B(tài)數(shù)據(jù)的閉包。完整代碼如下:
const Form = () => { const [value, setValue] = useState(); const ref = useRef(); useEffect(() => { ref.current = () => { // will be latest console.log(value); }; }); const onClick = useCallback(() => { // will be latest ref.current?.(); }, []); return ( <> <input type="text" value={value} onChange={(e) => setValue(e.target.value)} /> <HeavyComponentMemo title="你好 code秘密花園" onClick={onClick} /> </> ); };
現(xiàn)在,我們獲得了兩全其美的結(jié)果:Heavy 組件被適當(dāng)?shù)?memoization,不會(huì)因?yàn)槊看螤顟B(tài)變化而重新渲染。它的 onClick 回調(diào)可以訪問(wèn)組件中的最新數(shù)據(jù),而不會(huì)破壞 memoization?,F(xiàn)在,我們可以安全地將所需的一切發(fā)送到后端!
最后
下面我們?cè)倏偨Y(jié)一下本文中提到的知識(shí)點(diǎn):
- 每次在另一個(gè)函數(shù)內(nèi)部創(chuàng)建一個(gè)函數(shù)時(shí),都會(huì)形成閉包。
- 由于 React 組件只是函數(shù),因此內(nèi)部創(chuàng)建的每個(gè)函數(shù)都會(huì)形成閉包,包括 useCallback 和 useRef 等鉤子。
- 當(dāng)一個(gè)形成閉包的函數(shù)被調(diào)用時(shí),它周圍的所有數(shù)據(jù)都會(huì)被 "凍結(jié)",就像快照一樣。
- 要更新這些數(shù)據(jù),我們需要重新創(chuàng)建 "閉包" 函數(shù)。這就是使用 useCallback 等鉤子的依賴關(guān)系允許我們做的事情。
- 如果我們錯(cuò)過(guò)了依賴關(guān)系,或者沒(méi)有刷新分配給 ref.current 的閉包函數(shù),閉包就會(huì) "過(guò)期"。
- 在 React 中,我們可以利用 Ref 是一個(gè)可變對(duì)象這一特性,從而擺脫 "過(guò)期閉包" 的問(wèn)題。我們可以在過(guò)期閉包之外更改 ref.current,然后在閉包之內(nèi)訪問(wèn)它,就可以獲取最新的數(shù)據(jù)。
以上就是詳解如何在React中逃離閉包陷阱的詳細(xì)內(nèi)容,更多關(guān)于React逃離閉包陷阱的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mybatis(ParameterType)傳遞多個(gè)不同類型的參數(shù)方式
這篇文章主要介紹了Mybatis(ParameterType)傳遞多個(gè)不同類型的參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04MybatisPlus?BaseMapper?實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)增刪改查源碼
MybatisPlus?是一款在?Mybatis?基礎(chǔ)上進(jìn)行的增強(qiáng)?orm?框架,可以實(shí)現(xiàn)不寫?sql?就完成數(shù)據(jù)庫(kù)相關(guān)的操作,這篇文章主要介紹了MybatisPlus?BaseMapper?實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)增刪改查源碼解析,需要的朋友可以參考下2023-01-01log4j控制臺(tái)不打印日志故障的詳細(xì)解決方案
這篇文章主要給大家介紹了關(guān)于log4j控制臺(tái)不打印日志故障的詳細(xì)解決方案,log4j不提供默認(rèn)配置,因?yàn)樵谀承┉h(huán)境中可能禁止輸出到控制臺(tái)或文件系統(tǒng),需要的朋友可以參考下2023-08-08一文總結(jié)RabbitMQ中的消息確認(rèn)機(jī)制
RabbitMQ消息確認(rèn)機(jī)制指的是在消息傳遞過(guò)程中,發(fā)送方發(fā)送消息后,接收方需要對(duì)消息進(jìn)行確認(rèn),以確保消息被正確地接收和處理,本文為大家整理了RabbitMQ中的消息確認(rèn)機(jī)制,需要的可以參考一下2023-06-06如何開(kāi)發(fā)一個(gè)簡(jiǎn)單的Akka Java應(yīng)用
這篇文章主要介紹了如何開(kāi)發(fā)一個(gè)簡(jiǎn)單的Akka Java應(yīng)用 ,幫助大家使用Java創(chuàng)建Akka項(xiàng)目并將其打包,感興趣的朋友可以了解下2020-10-10Java代碼實(shí)現(xiàn)簡(jiǎn)單酒店管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java代碼實(shí)現(xiàn)簡(jiǎn)單酒店管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06