欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解如何在React中逃離閉包陷阱

 更新時(shí)間:2023年09月25日 09:04:53   作者:QdFe  
眾所周知,JavaScript 中的閉包(Closures)一定是這種語(yǔ)言最可怕的特性之一,另外它可能也是最隱蔽的語(yǔ)言特性之一,我們?cè)诰帉?nbsp;React 代碼時(shí)經(jīng)常會(huì)用到它,但是大多數(shù)時(shí)候我們甚至沒(méi)有意識(shí)到這一點(diǎn),本文小編將和大家一起深入探討如何在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)文章

最新評(píng)論