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