從 React 原理來看 ahooks 是怎麼解決 React 的閉包問題的?

来源:https://www.cnblogs.com/gopal/archive/2022/08/11/16573868.html
-Advertisement-
Play Games

本文是深入淺出 ahooks 源碼系列文章的第三篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本文來探索一下 ahooks 是怎麼解決 React 的閉包問題的?。 React 的閉包問題 先來看一個例子: import React, { useState, ...


本文是深入淺出 ahooks 源碼系列文章的第三篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。

本文來探索一下 ahooks 是怎麼解決 React 的閉包問題的?。

React 的閉包問題

先來看一個例子:

import React, { useState, useEffect } from "react";

export default () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      console.log("setInterval:", count);
    }, 1000);
  }, []);

  return (
    <div>
      count: {count}
      <br />
      <button onClick={() => setCount((val) => val + 1)}>增加 1</button>
    </div>
  );
};

代碼示例

當我點擊按鈕的時候,發現 setInterval 中列印出來的值並沒有發生變化,始終都是 0。這就是 React 的閉包問題。

產生的原因

為了維護 Function Component 的 state,React 用鏈表的方式來存儲 Function Component 裡面的 hooks,併為每一個 hooks 創建了一個對象。

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};

這個對象的 memoizedState 屬性就是用來存儲組件上一次更新後的 statenext 指向下一個 hook 對象。在組件更新的過程中,hooks 函數執行的順序是不變的,就可以根據這個鏈表拿到當前 hooks 對應的 Hook 對象,函數式組件就是這樣擁有了state的能力

同時制定了一系列的規則,比如不能將 hooks 寫入到 if...else... 中。從而保證能夠正確拿到相應 hook 的 state。

useEffect 接收了兩個參數,一個回調函數和一個數組。數組裡面就是 useEffect 的依賴,當為 [] 的時候,回調函數只會在組件第一次渲染的時候執行一次。如果有依賴其他項,react 會判斷其依賴是否改變,如果改變了就會執行回調函數。

回到剛剛那個例子:

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

useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, []);

它第一次執行的時候,執行 useState,count 為 0。執行 useEffect,執行其回調中的邏輯,啟動定時器,每隔 1s 輸出 setInterval: 0

當我點擊按鈕使 count 增加 1 的時候,整個函數式組件重新渲染,這個時候前一個執行的鏈表已經存在了。useState 將 Hook 對象 上保存的狀態置為 1, 那麼此時 count 也為 1 了。但是執行 useEffect,其依賴項為空,不執行回調函數。但是之前的回調函數還是在的,它還是會每隔 1s 執行 console.log("setInterval:", count);,但這裡的 count 是之前第一次執行時候的 count 值,因為在定時器的回調函數裡面被引用了,形成了閉包一直被保存。

解決的方法

解決方法一:給 useEffect 設置依賴項,重新執行函數,設置新的定時器,拿到最新值。

// 解決方法一
useEffect(() => {
  if (timer.current) {
    clearInterval(timer.current);
  }
  timer.current = setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, [count]);

解決方法二:使用 useRef。
useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(initialValue)。

useRef 創建的是一個普通 Javascript 對象,而且會在每次渲染時返回同一個 ref 對象,當我們變化它的 current 屬性的時候,對象的引用都是同一個,所以定時器中能夠讀到最新的值。

const lastCount = useRef(count);

// 解決方法二
useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", lastCount.current);
  }, 1000);
}, []);

return (
  <div>
    count: {count}
    <br />
    <button
      onClick={() => {
        setCount((val) => val + 1);
        // +1
        lastCount.current += 1;
      }}
    >
      增加 1
    </button>
  </div>
);

useRef => useLatest

終於回到我們 ahooks 主題,基於上述的第二種解決方案,useLatest 這個 hook 隨之誕生。它返回當前最新值的 Hook,可以避免閉包問題。實現原理很簡單,只有短短的十行代碼,就是使用 useRef 包一層:

import { useRef } from 'react';
// 通過 useRef,保持每次獲取到的都是最新的值
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

export default useLatest;

useEvent => useMemoizedFn

React 中另一個場景,是基於 useCallback 的。

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

const callbackFn = useCallback(() => {
  console.log(`Current count is ${count}`);
}, []);

以上不管,我們的 count 的值變化成多少,執行 callbackFn 列印出來的 count 的值始終都是 0。這個是因為回調函數被 useCallback 緩存,形成閉包,從而形成閉包陷阱。

那我們怎麼解決這個問題呢?官方提出了 useEvent。它解決的問題:如何同時保持函數引用不變與訪問到最新狀態。使用它之後,上面的例子就變成了。

const callbackFn = useEvent(() => {
  console.log(`Current count is ${count}`);
});

在這裡我們不細看這個特性,實際上,在 ahooks 中已經實現了類似的功能,那就是 useMemoizedFn。

useMemoizedFn 是持久化 function 的 Hook,理論上,可以使用 useMemoizedFn 完全代替 useCallback。使用 useMemoizedFn,可以省略第二個參數 deps,同時保證函數地址永遠不會變化。以上的問題,通過以下的方式就能輕鬆解決:

const memoizedFn = useMemoizedFn(() => {
  console.log(`Current count is ${count}`);
});

Demo 地址

我們來看下它的源碼,可以看到其還是通過 useRef 保持 function 引用地址不變,並且每次執行都可以拿到最新的 state 值。

function useMemoizedFn<T extends noop>(fn: T) {
  // 通過 useRef 保持其引用地址不變,並且值能夠保持值最新
  const fnRef = useRef<T>(fn);
  fnRef.current = useMemo(() => fn, [fn]);
  // 通過 useRef 保持其引用地址不變,並且值能夠保持值最新
  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    // 返回的持久化函數,調用該函數的時候,調用原始的函數
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

總結與思考

React 自從引入 hooks,雖然解決了 class 組件的一些弊端,比如邏輯復用需要通過高階組件層層嵌套等。但是也引入了一些問題,比如閉包問題。

這個是 React 的 Function Component State 管理導致的,有時候會讓開發者產生疑惑。開發者可以通過添加依賴或者使用 useRef 的方式進行避免。

ahooks 也意識到了這個問題,通過 useLatest 保證獲取到最新的值和 useMemoizedFn 持久化 function 的方式,避免類似的閉包陷阱。

值得一提的是 useMemoizedFn 是 ahooks 輸出函數的標準,所有的輸出函數都使用 useMemoizedFn 包一層。另外輸入函數都使用 useRef 做一次記錄,以保證在任何地方都能訪問到最新的函數。

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 昨晚我正在床上睡得著著的,突然來了一條簡訊。 什麼?線上的訂單無法取消! 我趕緊登錄線上系統,查看業務日誌。發現有MySQL鎖超時的錯誤日誌。 ...
  • 課件獲取:關註公眾號“數棧研習社”,後臺私信 “ChengYing” 獲得直播課件 視頻回放:點擊這裡 ChengYing開源項目地址:github 丨 gitee 喜歡我們的項目給我們點個__ STAR!STAR!!STAR!!!(重要的事情說三遍)__ 技術交流釘釘 qun:30537511 本 ...
  • 首先nosql可以被理解為not only sql 泛指非關係型資料庫,也就是說不僅僅是sql,所以它既包含了sql的一些東西,但是又和sql不同,併在其的基礎上改變或者說擴展了一些東西。 提到nosql,首先我們就要分析一下關係型資料庫的行式存儲和非關係型資料庫的列式存儲區別在哪? 行式存儲我們都 ...
  • 原文:Jetpack Compose學習(9)——Compose中的列表控制項(LazyRow和LazyColumn) - Stars-One的雜貨小窩 經過前面的學習,大致上已掌握了compose的基本使用了,本篇繼續進行擴展,講解下載Compose中的列表控制項LazyRow和LazyColumn ...
  • 數據訂閱是運動健康類應用中很常見的功能,用戶訂閱運動記錄、健康記錄等數據,當這些數據發生變化時,用戶能夠即時在App上接收到推送通知。 例如某位用戶最近正在鍛煉身體,為自己設定了每天走1萬步,每周達成3次的目標;常規的數據訂閱可以做到每天給用戶推送當天步數,但用戶需要自己進行二次計算才能得知自己是否 ...
  • 盒模型 點擊打開視頻教程 標準盒模型、怪異盒模型(IE盒模型) 什麼是盒模型? 盒模型的作用:規定了網頁元素如何顯示以及元素間的相互關係 盒模型的概念:盒模型是css佈局的基石,它規定了網頁元素如何顯示以及元素間的相互關係。 css定義所有的元素都可以擁有像盒子一樣的外形和平面空間。即都包含內容區、 ...
  • 背景 項目中用到了vue的element-ui框架,用到了el-tree組件。由於數據量很大,使用了數據懶載入模式,即非同步樹。非同步樹採用覆選框進行結點選擇的時候,沒法自動展開,官方文檔找了半天也沒有找到好的辦法! 找不到相關的配置,或者方法可以使用。 經過調試與閱讀elment-ui源碼才發現有現成 ...
  • 有時候我們不希望組件被重新渲染影響使用體驗;或者處於性能考慮,避免多次重覆渲染降低性能。而是希望組件可以緩存下來,維持當前的狀態。這時候就需要用到keep-alive組件。 開啟keep-alive 生命周期的變化. 初次進入時: onMounted> onActivated 退出後觸發 deact ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...