深入解析React?Hooks?閉包陷阱
正文
React Hooks 是 React 16.8 版本引入的一種新的特性,它允許我們在不編寫 class 組件的情況下使用 state 以及其他的 React 功能。其中,最為常用的就是 useState 和 useEffect。在使用 React Hooks 時(shí),由于函數(shù)組件沒有實(shí)例,所以 Hooks 靠的是閉包來訪問和更新 state。但是,在使用 Hooks 時(shí),我們需要注意閉包陷阱問題。
什么是閉包陷阱?
閉包是指一個(gè)函數(shù)可以訪問定義在函數(shù)外部的變量。在 React 中,Hooks 函數(shù)也是閉包,它們可以訪問定義在函數(shù)外部的變量。React Hooks 的閉包陷阱與普通 JavaScript 中的閉包陷阱類似,但是由于 React Hooks 的設(shè)計(jì),使用 Hooks 時(shí)可能會(huì)遇到一些特定的問題。
React Hooks 中的閉包陷阱主要會(huì)發(fā)生在兩種情況:
- 在 useState 中使用閉包;
- 在 useEffect 中使用閉包。
useState 中的閉包陷阱
在useState中使用閉包,主要是因?yàn)閡seState的參數(shù)只會(huì)在組件掛載時(shí)執(zhí)行一次。如果我們在useState中使用閉包,那么閉包中的變量值會(huì)被緩存,這意味著當(dāng)我們在組件中更新狀態(tài)時(shí),閉包中的變量值不會(huì)隨之更新。
示例
React Hooks 的閉包陷阱發(fā)生在 useState 鉤子函數(shù)中的示例,如下:
function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setTimeout(() => { setCount(count + 1); }, 1000); }; const handleReset = () => { setCount(0); }; return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> <button onClick={handleReset}>Reset</button> </div> ); }
在上面的代碼中,我們定義了一個(gè)handleClick函數(shù),它使用了一個(gè)閉包來緩存count的值。然而,由于閉包中的count值被緩存了,這意味著即使我們在1秒后調(diào)用setCount方法來更新count的值,閉包中的count值仍然是舊的值。因此,如果我們點(diǎn)擊Increment按鈕,即使我們重復(fù)點(diǎn)擊多次,計(jì)數(shù)器也只會(huì)增加1次。
避免方法
為了解決這個(gè)問題,我們需要使用React Hooks提供的更新函數(shù)的形式來更新狀態(tài)。我們可以把handleClick函數(shù)改成這樣:
const handleClick = () => { setTimeout(() => { setCount(count => count + 1); }, 1000); };
在這個(gè)版本的handleClick函數(shù)中,我們使用了setCount的更新函數(shù)形式。這個(gè)函數(shù)會(huì)接收count的當(dāng)前值作為參數(shù),這樣我們就可以在閉包中使用這個(gè)值,而不需要擔(dān)心它被緩存。
useEffect 的閉包陷阱
在useEffect中使用閉包的問題則是因?yàn)閡seEffect中的函數(shù)是在每次組件更新時(shí)都會(huì)執(zhí)行一次。如果我們在useEffect中使用閉包,那么這個(gè)閉包中的變量值也會(huì)被緩存,這樣就可能會(huì)導(dǎo)致一些問題。
示例
React Hooks 中的閉包陷阱通常發(fā)生在 useEffect 鉤子函數(shù)中,例如:
function App() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); }, 1000); return () => clearInterval(timer); }, []); const handleClick = () => { setCount(count + 1); }; return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> </div> ); }
在這個(gè)例子中,我們使用了 useState 和 useEffect Hooks。在 useEffect 回調(diào)函數(shù)內(nèi)部,我們使用了一個(gè) setTimeout 函數(shù)來更新 count 狀態(tài)變量。然而,由于 useEffect 只會(huì)在組件首次渲染時(shí)執(zhí)行一次,因此閉包中的 count 變量始終是首次渲染時(shí)的變量,而不是最新的值。
避免方法
為了避免這種閉包陷阱,可以使用 useEffect Hook 來更新狀態(tài)。例如,以下代碼中,通過 useEffect Hook 來更新 count 的值,就可以避免閉包陷阱:
useEffect(() => { const timer = setInterval(() => { console.log(count); }, 1000); return () => clearInterval(timer); }, [count]);
通過閉包訪問和更新 state
在 React 中,class 組件可以使用 this.state 和 this.setState 來管理組件的狀態(tài)。這是因?yàn)?class 組件具有實(shí)例,可以將狀態(tài)存儲(chǔ)在實(shí)例屬性中,以便在組件的生命周期方法和事件處理程序中訪問和更新。
而函數(shù)組件則沒有實(shí)例,無法將狀態(tài)存儲(chǔ)在實(shí)例屬性中。為了解決這個(gè)問題,React 引入了 React Hooks,其中最為常用的是 useState。useState 允許我們在函數(shù)組件中使用 state,而無需編寫 class 組件。
useState 是通過閉包來實(shí)現(xiàn)的。當(dāng)我們調(diào)用 useState 時(shí),它會(huì)返回一個(gè)數(shù)組,其中第一個(gè)元素是當(dāng)前狀態(tài)的值,第二個(gè)元素是更新狀態(tài)的函數(shù)。例如:
import React, { useState } from 'react'; const Counter = () => { const [count, setCount] = useState(0); // ... };
在這個(gè)例子中,useState 的初始值為 0,useState 的返回值是一個(gè)數(shù)組 [count, setCount],其中 count 是當(dāng)前狀態(tài)的值,setCount 是更新狀態(tài)的函數(shù)。
當(dāng)我們在組件內(nèi)部調(diào)用 setCount 函數(shù)時(shí),React 會(huì)在內(nèi)部使用閉包來訪問和更新 count 變量。這是因?yàn)?,useState 是在組件的頂層作用域中調(diào)用的,而 setCount 函數(shù)是在組件的事件處理程序中調(diào)用的。這意味著,setCount 函數(shù)需要訪問 count 變量,但是 count 變量無法存儲(chǔ)在實(shí)例屬性中。
為了解決這個(gè)問題,React 使用了閉包,將 count 變量保存在內(nèi)部函數(shù)中。當(dāng)組件重新渲染時(shí),React 會(huì)創(chuàng)建一個(gè)新的閉包,并將 count 變量的值更新為新的狀態(tài)值。這個(gè)新的閉包會(huì)在下一次調(diào)用 setCount 函數(shù)時(shí)被使用。
下面是一個(gè)例子,展示了 useState 如何通過閉包來訪問和更新 state 的:
import React, { useState } from 'react'; const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return ( <> <p>You clicked {count} times</p> <button onClick={handleClick}>Click me</button> </> ); };
在這個(gè)例子中,我們調(diào)用 useState,并將初始值設(shè)置為 0。在組件內(nèi)部,我們創(chuàng)建了一個(gè) handleClick 函數(shù),并調(diào)用 setCount 函數(shù)來更新 count 的值。由于 setCount 函數(shù)是在 handleClick 函數(shù)中調(diào)用的,因此需要使用閉包來訪問和更新 count 變量。
需要注意的是,由于閉包的作用,如果我們在組件的事件處理程序中訪問了過時(shí)的 state,可能會(huì)導(dǎo)致組件的狀態(tài)出現(xiàn)錯(cuò)誤。為了避免這種情況,我們需要使用 React Hooks 提供的其他功能,例如 useEffect 和 useCallback。這些功能可以幫助我們避免閉包陷阱,確保組件的狀態(tài)更新正確地渲染到視圖上。
從 React Hooks 源碼看閉包陷阱
React Hooks 中閉包陷阱的問題源于 useState 等 Hooks 的實(shí)現(xiàn)方式。在 React 內(nèi)部,每個(gè)組件都有一個(gè)對(duì)應(yīng)的 Fiber 對(duì)象,表示組件的渲染狀態(tài)。useState 等 Hooks 的實(shí)現(xiàn)都是基于這個(gè) Fiber 對(duì)象的,并且會(huì)在 Fiber 對(duì)象中存儲(chǔ)當(dāng)前狀態(tài)值和更新狀態(tài)的函數(shù)。
例如,在 useState Hook 中,會(huì)通過調(diào)用 useStateImpl 函數(shù)來獲取當(dāng)前狀態(tài)值和更新狀態(tài)的函數(shù):
function useState(initialState) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function useStateImpl(initialState) { const hook = mountState(initialState); return [hook.memoizedState, dispatchAction.bind(null, hook.queue)]; }
其中,mountState 函數(shù)是用來初始化 Hook 對(duì)象的。它會(huì)檢查當(dāng)前 Fiber 對(duì)象上是否已經(jīng)存在對(duì)應(yīng)的 Hook,如果存在的話就直接返回該 Hook,否則就創(chuàng)建一個(gè)新的 Hook 對(duì)象并存儲(chǔ)到當(dāng)前 Fiber 對(duì)象上:
function mountState(initialState) { const currentHook = updateQueue.next; if (currentHook !== null) { updateQueue.next = currentHook.next; return currentHook; } else { const newHook = { memoizedState: typeof initialState === 'function' ? initialState() : initialState, queue: [], next: null, }; if (updateQueue.last === null) { updateQueue.first = updateQueue.last = newHook; } else { updateQueue.last = updateQueue.last.next = newHook; } return newHook; } }
需要注意的是,每個(gè) Hook 對(duì)象中都有一個(gè) queue 屬性,用來存儲(chǔ)更新狀態(tài)的 action。而 dispatchAction 函數(shù)則是用來觸發(fā)更新的:
function dispatchAction(queue, action) { const update = { action, next: null, }; if (queue.last === null) { queue.first = queue.last = update; } else { queue.last = queue.last.next = update; } scheduleWork(); }
在組件重新渲染時(shí),React 會(huì)重新執(zhí)行函數(shù)組件的函數(shù)體,從而調(diào)用 useState 等 Hook 重新獲取狀態(tài)值和更新狀態(tài)的函數(shù)。由于每次重新渲染都會(huì)創(chuàng)建一個(gè)新的 Fiber 對(duì)象,因此在新的 Fiber 對(duì)象上獲取到的 Hook 對(duì)象和狀態(tài)值都是新的。
然而,由于更新狀態(tài)的函數(shù)是存儲(chǔ)在 Hook 對(duì)象中的,因此會(huì)造成更新函數(shù)的閉包引用的是舊的狀態(tài)值,而不是最新的狀態(tài)值。例如,在以下代碼中,每次點(diǎn)擊按鈕都會(huì)增加 count 的值,但是打印出來的 count 值卻始終為 1,這是因?yàn)?setCount 使用的是 count 的初始值,而不是最新的值,因?yàn)?setCount 是在一個(gè)閉包中定義的:
function Counter() { let count = 0; const [visible, setVisible] = useState(false); function handleClick() { count++; console.log(count); setVisible(!visible); } return ( <> <button onClick={handleClick}>Click me</button> {visible && <div>Count: {count}</div>} </> ); }
以上就是 React Hooks 閉包陷阱的詳細(xì)內(nèi)容,更多關(guān)于 React Hooks 閉包陷阱的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何在React?Native開發(fā)中防止滑動(dòng)過程中的誤觸
在使用React?Native開發(fā)的時(shí),當(dāng)我們快速滑動(dòng)應(yīng)用的時(shí)候,可能會(huì)出現(xiàn)誤觸,導(dǎo)致我們會(huì)點(diǎn)擊到頁面中的某一些點(diǎn)擊事件,誤觸導(dǎo)致頁面元素響應(yīng)從而進(jìn)行其他操作,表現(xiàn)出非常不好的用戶體驗(yàn)。2023-05-05React實(shí)時(shí)預(yù)覽react-live源碼解析
這篇文章主要為大家介紹了React實(shí)時(shí)預(yù)覽react-live源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08解決React報(bào)錯(cuò)Property 'X' does not 
這篇文章主要為大家介紹了解決React報(bào)錯(cuò)Property 'X' does not exist on type 'HTMLElement',有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12