最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文了解React Hooks 的閉包機制

 更新時間:2026年07月23日 09:59:02   作者:weedsfly  
在React項目里,你是否遇到過setTimeout、Promise或事件監(jiān)聽器中狀態(tài)總是舊值的詭異情況?這就是“過期閉包”陷阱,本文留來詳細的介紹一下解決方法,感興趣的可以了解一下

如果你在 React 項目里使用過 setTimeout、Promise 或者事件監(jiān)聽器,大概率遇到過這種詭異情況:明明狀態(tài)已經(jīng)更新了,但在回調(diào)里拿到的卻還是舊值。這種“過期閉包”是 Hooks 時代最常見的陷阱之一。本文會從四個真實的開發(fā)場景出發(fā),深入剖析問題根源,并給出函數(shù)式更新、useRef、useEffect 依賴和 useLatest 四個核心解決方案。

一、從一次“定時讀取狀態(tài)”說起

假設(shè)你在做一個計數(shù)器應(yīng)用,產(chǎn)品提了一個需求:點擊按鈕后,延時 2 秒再打印當前的計數(shù)。你快速寫出如下代碼:

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(`當前 count:${count}`);
    }, 2000);
  };

  return <button onClick={handleClick}>count: {count}</button>;
}

你滿懷期待地快速點擊兩次按鈕,希望 2 秒后控制臺分別打印 12。然而結(jié)果卻是兩個 0,或者第一個是 0,第二個是 1——總之不是期望的最新值。這就是過期閉包。

二、根本原因:每一次渲染都是一次獨立的閉包

函數(shù)組件本質(zhì)上是一個函數(shù),每次狀態(tài)更新都會重新執(zhí)行,生成一次全新的渲染。而 每一次渲染都有自己獨立的 propsstate,這些值就像是該渲染幀的“快照”,被保存在該幀的閉包中。

setTimeout 回調(diào)在創(chuàng)建時綁定了當前渲染幀的 count。即便之后組件因為 setCount 重新渲染,count 在后續(xù)幀中已經(jīng)變成了新值,但第一個定時器仍然活在過去,緊緊抓住它誕生時那個舊的 count。

在 React 18 中,雖然引入了自動批處理,讓連續(xù)更新只觸發(fā)一次重渲染,但這并不能解決閉包陳舊的問題——閉包中的變量依然還是創(chuàng)建時的值。

三、四種典型的過期閉包場景與解決方案

場景一:setTimeout中的過期閉包

const handleClick = () => {
  setCount(count + 1);
  setTimeout(() => {
    console.log(count); // 永遠打印點擊前的值
  }, 2000);
};

解決方案:useRef+ 手動同步

useRef 會返回一個在組件整個生命周期中保持不變的引用對象,其 current 屬性可以被隨意修改而不會觸發(fā)重渲染。我們可以利用它來保存“最新值”,從而繞過閉包限制。

function Counter() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    latestCount.current = count; // 每次渲染后同步最新值
  });

  const handleClick = () => {
    setCount(prev => prev + 1);
    setTimeout(() => {
      console.log(`最新 count:${latestCount.current}`);
    }, 2000);
  };

  return <button onClick={handleClick}>count: {count}</button>;
}

這樣,定時器回調(diào)不再依賴閉包中的 count,而是通過 ref 這個“可變的橋”拿到最新值。

進階封裝:useLatest

上面的模式可以封裝成一個輕量級的自定義 Hook,像 ahooks 里的 useLatest 那樣:

function useLatest(value) {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  });
  return ref;
}

// 使用
const latestCount = useLatest(count);
setTimeout(() => {
  console.log(latestCount.current);
}, 2000);

代碼語義清晰明了:我需要在這個異步回調(diào)里,訪問那個“未來時刻”的最新值。

場景二:Promise異步操作中的過期狀態(tài)與競態(tài)條件

function UserProfile({ userId }) {
  const [name, setName] = useState('');

  const fetchUser = async () => {
    const data = await api.getUser(userId); // userId 是閉包中的值
    setName(data.name);
  };

  return <button onClick={fetchUser}>獲取用戶</button>;
}

如果 userId 在請求過程中發(fā)生了改變,不僅可能拿到錯誤的用戶數(shù)據(jù),后返回的舊請求還可能覆蓋新請求的結(jié)果,造成經(jīng)典的競態(tài)條件。

解決方案一:useRef跟蹤最新 ID + 丟棄過期響應(yīng)

function UserProfile({ userId }) {
  const [name, setName] = useState('');
  const latestUserId = useRef(userId);
  useEffect(() => {
    latestUserId.current = userId;
  });

  const fetchUser = async () => {
    const currentUserId = latestUserId.current;
    const data = await api.getUser(currentUserId);
    if (currentUserId === latestUserId.current) { // 保證響應(yīng)對應(yīng)的請求仍是“當前”的
      setName(data.name);
    }
  };
}

解決方案二:使用useEffect正確聲明依賴

如果場景是“userId 變化就自動重新獲取”,那就應(yīng)該把副作用交給 useEffect,并誠實聲明依賴:

useEffect(() => {
  let cancelled = false;
  api.getUser(userId).then(data => {
    if (!cancelled) setName(data.name);
  });
  return () => { cancelled = true; };
}, [userId]); // userId 變化時自動重新執(zhí)行

清理函數(shù)中的標志位可以阻止已過期請求的副作用。這是 React 中處理異步的經(jīng)典范式,同時也解決了過期閉包問題——因為每次 userId 變化都會創(chuàng)建新的 Effect 閉包,它自然能拿到最新的 userId。

場景三:事件監(jiān)聽器中的閉包陳舊

function MouseTracker() {
  const [logEnabled, setLogEnabled] = useState(false);

  useEffect(() => {
    const handleMove = (e) => {
      if (logEnabled) {
        console.log(`位置: ${e.clientX}, ${e.clientY}`);
      }
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []); // 空依賴,logEnabled 永遠為 false
}

無論之后如何開關(guān)日志,控制臺都靜悄悄,因為事件監(jiān)聽器在首次渲染時就被綁定,其閉包中的 logEnabled 已經(jīng)定格為 false

解決方案:useRef存儲最新值

function MouseTracker() {
  const [logEnabled, setLogEnabled] = useState(false);
  const logEnabledRef = useRef(logEnabled);

  useEffect(() => {
    logEnabledRef.current = logEnabled;
  });

  useEffect(() => {
    const handleMove = (e) => {
      if (logEnabledRef.current) {
        console.log(`位置: ${e.clientX}, ${e.clientY}`);
      }
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []); // 事件僅注冊一次,但讀的是 ref
}

通過 ref,我們成功將事件監(jiān)聽器的注冊和最新狀態(tài)的讀取解耦。

場景四:useEffect內(nèi)定時器依賴缺失

function Timer() {
  const [count, setCount] = useState(0);
  const [delay, setDelay] = useState(1000);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // 閉包中的 count 始終為 0
    }, delay);
    return () => clearInterval(id);
  }, []); // 空依賴,effect 只執(zhí)行一次
}

無論怎么改變 delay,定時器間隔始終是 1000ms,且 count 會一直在 1 上反復(fù)更新(因為每次都是 0 + 1)。

解決方案:函數(shù)式更新 + 正確依賴

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1); // 不依賴外部 count
  }, delay);
  return () => clearInterval(id);
}, [delay]); // delay 變化時重新創(chuàng)建定時器

函數(shù)式更新 setState(prev => prev + 1) 讓你在不需要閉包中 count 的情況下,基于最新狀態(tài)進行計算。它和 useRef 一樣,都是解決過期閉包的核心武器,但適用場景不同:函數(shù)式更新只能用于“基于自身舊值更新自身”,而 useRef 更適合需要同時讀取多個其他狀態(tài)或 props 的場景。

四、總結(jié)

React 的函數(shù)組件和 Hooks 讓代碼更簡潔,但“每次渲染獨立閉包”的心智模型也讓過期閉包成為高頻痛點。與其記住零碎的寫法,不如抓住核心原則:如果你在一個長時間存活的閉包(定時器、Promise、事件監(jiān)聽器)里需要訪問 state 或 props,不要直接讀取閉包中的值,而是通過 ref 或者函數(shù)式更新來獲取那個“未來時刻”的真實狀態(tài)。

到此這篇關(guān)于一文了解React Hooks 的閉包機制的文章就介紹到這了,更多相關(guān)React Hooks 閉包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React?Fiber構(gòu)建源碼解析

    React?Fiber構(gòu)建源碼解析

    這篇文章主要為大家介紹了React?Fiber構(gòu)建源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • 如何在React項目中引入字體文件并使用詳解

    如何在React項目中引入字體文件并使用詳解

    我們項目中通常會需要引入字體,所以下面這篇文章主要給大家介紹了關(guān)于如何在React項目中引入字體文件并使用的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下
    2022-08-08
  • 詳解開發(fā)react應(yīng)用最好用的腳手架 create-react-app

    詳解開發(fā)react應(yīng)用最好用的腳手架 create-react-app

    本篇文章主要介紹了詳解開發(fā)react應(yīng)用最好用的腳手架 create-react-app,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • 使用reactjs優(yōu)化了進度條頁面性能提高70%

    使用reactjs優(yōu)化了進度條頁面性能提高70%

    這篇文章主要介紹了使用reactjs優(yōu)化了進度條后頁面性能提高了70%的操作技巧,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-04-04
  • react中使用useEffect及踩坑記錄

    react中使用useEffect及踩坑記錄

    這篇文章主要介紹了react中使用useEffect及踩坑記錄,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • 使用webpack5從0到1搭建一個react項目的實現(xiàn)步驟

    使用webpack5從0到1搭建一個react項目的實現(xiàn)步驟

    這篇文章主要介紹了使用webpack5從0到1搭建一個react項目的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-12-12
  • 解讀react的onClick自動觸發(fā)等相關(guān)問題

    解讀react的onClick自動觸發(fā)等相關(guān)問題

    這篇文章主要介紹了解讀react的onClick自動觸發(fā)等相關(guān)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • React styled-components設(shè)置組件屬性的方法

    React styled-components設(shè)置組件屬性的方法

    這篇文章主要介紹了styled-components設(shè)置組件屬性的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08
  • 如何在React中通過URL預(yù)覽Excel文件

    如何在React中通過URL預(yù)覽Excel文件

    這篇文章主要為大家詳細介紹了如何在React中通過URL預(yù)覽Excel文件,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下
    2025-03-03
  • React中useEffect 與 useLayoutEffect的區(qū)別

    React中useEffect 與 useLayoutEffect的區(qū)別

    本文主要介紹了React中useEffect與useLayoutEffect的區(qū)別,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-07-07

最新評論

长葛市| 昭平县| 沁水县| 衡阳市| 新干县| 綦江县| 三都| 阜南县| 祁连县| 招远市| 巧家县| 安吉县| 钟祥市| 秦安县| 民丰县| 会宁县| 遵义市| 桂林市| 安乡县| 海南省| 青神县| 恭城| 密云县| 精河县| 石首市| 灵石县| 通化市| 花莲县| 弥渡县| 德令哈市| 凯里市| 敦化市| 武隆县| 山丹县| 新兴县| 东丰县| 阳信县| 雅江县| 英超| 闻喜县| 龙胜|