從 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
  • 一個自定義WPF窗體的解決方案,借鑒了呂毅老師的WPF製作高性能的透明背景的異形視窗一文,併在此基礎上增加了滑鼠穿透的功能。可以使得透明窗體的滑鼠事件穿透到下層,在下層窗體中響應。 ...
  • 在C#中使用RabbitMQ做個簡單的發送郵件小項目 前言 好久沒有做項目了,這次做一個發送郵件的小項目。發郵件是一個比較耗時的操作,之前在我的個人博客裡面回覆評論和友鏈申請是會通過發送郵件來通知對方的,不過當時只是簡單的進行了非同步操作。 那麼這次來使用RabbitMQ去統一發送郵件,我的想法是通過 ...
  • 當你使用Edge等瀏覽器或系統軟體播放媒體時,Windows控制中心就會出現相應的媒體信息以及控制播放的功能,如圖。 SMTC (SystemMediaTransportControls) 是一個Windows App SDK (舊為UWP) 中提供的一個API,用於與系統媒體交互。接入SMTC的好 ...
  • 最近在微軟商店,官方上架了新款Win11風格的WPF版UI框架【WPF Gallery Preview 1.0.0.0】,這款應用引入了前沿的Fluent Design UI設計,為用戶帶來全新的視覺體驗。 ...
  • 1.簡單使用實例 1.1 添加log4net.dll的引用。 在NuGet程式包中搜索log4net並添加,此次我所用版本為2.0.17。如下圖: 1.2 添加配置文件 右鍵項目,添加新建項,搜索選擇應用程式配置文件,命名為log4net.config,步驟如下圖: 1.2.1 log4net.co ...
  • 之前也分享過 Swashbuckle.AspNetCore 的使用,不過版本比較老了,本次演示用的示例版本為 .net core 8.0,從安裝使用開始,到根據命名空間分組顯示,十分的有用 ...
  • 在 Visual Studio 中,至少可以創建三種不同類型的類庫: 類庫(.NET Framework) 類庫(.NET 標準) 類庫 (.NET Core) 雖然第一種是我們多年來一直在使用的,但一直感到困惑的一個主要問題是何時使用 .NET Standard 和 .NET Core 類庫類型。 ...
  • WPF的按鈕提供了Template模板,可以通過修改Template模板中的內容對按鈕的樣式進行自定義。結合資源字典,可以將自定義資源在xaml視窗、自定義控制項或者整個App當中調用 ...
  • 實現了一個支持長短按得按鈕組件,單擊可以觸發Click事件,長按可以觸發LongPressed事件,長按鬆開時觸發LongClick事件。還可以和自定義外觀相結合,實現自定義的按鈕外形。 ...
  • 一、WTM是什麼 WalkingTec.Mvvm框架(簡稱WTM)最早開發與2013年,基於Asp.net MVC3 和 最早的Entity Framework, 當初主要是為瞭解決公司內部開發效率低,代碼風格不統一的問題。2017年9月,將代碼移植到了.Net Core上,併進行了深度優化和重構, ...