從 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
  • 周末,寫點簡單的水一下。 新版本的vs創建項目的時候可以選擇自帶一個swagger。然而這隻是基本的swagger功能。 幾個介面無所謂啦,隨著介面越來越多,就這麼丟給你,一時間也會懵逼,所以這篇文章要做的有兩個功能。 給swagger文檔添加註釋 給swagger添加切換“版本”的功能(也可以理解 ...
  • 大家好,我是沙漠盡頭的狼。 本文首發於Dotnet9,介紹使用Lib.Harmony庫攔截第三方.NET庫方法,達到不修改其源碼並能實現修改方法邏輯、預期行為的效果,並且不限於只攔截public訪問修飾的類及方法,行文目錄: 什麼是方法攔截? 示常式序攔截 非public方法怎麼攔截? 總結 1. ...
  • 問題代碼: xmal:一個按鈕+一個顯示框 1 <Button Width="100" Height="50" Margin="10" Click="Button_Click">test</Button> 2 <TextBox x:Name="display" Width="300" Height= ...
  • 前置條件 ​ 阿裡雲伺服器一臺(可在購買伺服器時勾選安裝寶塔選項,免去後面的寶塔安裝) ​ 設置阿裡雲伺服器密碼並登陸伺服器 ​ 以下操作均在伺服器Linux中進行(使用遠程連接工具登錄) 寶塔登錄 登錄阿裡雲伺服器在Linux命令行中輸入bt,查看寶塔信息 ​ 根據寶塔信息提供的網站登陸寶塔服務( ...
  • GetTokenInformation 用於檢索進程或線程的令牌(Token)信息。Token是一個數據結構,其包含有關進程或線程的安全上下文,代表當前用戶或服務的安全標識符和許可權信息。GetTokenInformation函數也可以用來獲取這些安全信息,通常用於在運行時檢查某個進程或線程的許可權或安... ...
  • matplotlib 在1.0版本之前其實是不支持3D圖形繪製的。 後來的版本中,matplotlib加入了3D圖形的支持,不僅僅是為了使數據的展示更加生動和有趣。更重要的是,由於多了一個維度,擴展了其展示數據分佈和關係的能力,可以一次從三個維度來比較數據。 下麵介紹在matplotlib中繪製各類 ...
  • 編寫一個App就能編譯發佈到iOS、Android和Web等各大平臺的跨平臺技術,各大廠商一直都有研究和發佈對應技術產品,目前最熱門的莫過於Flutter框架了。而Dart作為其唯一的編程語言,今天我們開始來體驗一下…… ...
  • 實現基本的線程池 前提:我們要實現的線程池有如下功能: 基本的線程池模型 能提交和運行任務 能正常關閉線程池 線程的拒絕策略 線程池擴容 縮容線程池 代碼地址: 1、線程池的介紹? 線程池是什麼? 線程池是一種利用池化技術來管理線程的一種技術。 當沒有線程池的時候,我們如何創建線程? 繼承Threa ...
  • SDRAM基本信息 儲存能力計算 4X16X4=256(Mbit),註意不是MByte SDRAM控制 sdram包含兩個部分:sdram_ctrl、fifo_ctrl。 sdram_ctrl:其頂層為SDRAM的控制模塊內部實例化了5個模塊,有初始化、自刷新、寫和讀模塊,還有一個仲裁模塊對這四個不 ...
  • 歡迎訪問我的GitHub 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos 本篇概覽 欣宸正在為接下新的Java雲原生實戰系列原創做準備,既然是實戰,少不了一套雲原生環境,以下內容是必不可少的: linux操作系統 kuberne ...