理解 useMemo 和 useCallback


如果你一直在努力理解 useMemouseCallback翩迈,你并不孤單持灰!我已經和很多 React 開發(fā)者聊過,他們都對這兩個鉤子感到困惑帽馋。

在這篇博文中搅方,我的目標是澄清這些混亂。我們將學習它們的功能绽族、為什么有用以及如何最大限度地利用它們姨涡。

基本概念

好了,我們從 useMemo 開始吧慢。

useMemo 的基本思想是涛漂,它允許我們在渲染之間“記住”計算過的值。

這個定義需要進一步解釋检诗。實際上匈仗,它要求我們對 React 的工作機制有一個相當復雜的心理模型!所以我們先來解決這個問題逢慌。

React 的主要功能是使我們的 UI 與應用程序狀態(tài)保持同步悠轩。它使用的工具叫做“重新渲染”。

每次重新渲染都是應用程序在某一時刻基于當前狀態(tài)的 UI 快照攻泼。我們可以把它想象成一堆照片火架,每張照片捕捉到每個狀態(tài)變量在特定值下的樣子。


每次“重新渲染”會生成當前狀態(tài)下 DOM 應該呈現的樣子忙菠。在上面的演示中何鸡,它顯示為 HTML,但實際上是一些 JavaScript 對象牛欢。如果你聽說過“虛擬 DOM”這個詞骡男,就是在說這個概念。

我們不會直接告訴 React 哪些 DOM 節(jié)點需要更改傍睹。相反隔盛,我們告訴 React犹菱,基于當前狀態(tài),UI 應該是什么樣子骚亿。通過重新渲染已亥,React 創(chuàng)建了一個新快照熊赖,并通過比較快照来屠,像玩“找不同”游戲一樣,找出需要更改的地方震鹉。

React 本身經過了高度優(yōu)化俱笛,因此一般來說,重新渲染并不是什么大問題传趾。但是在某些情況下迎膜,生成這些快照可能需要一段時間。這會導致性能問題浆兰,例如用戶執(zhí)行操作后 UI 無法及時更新磕仅。

從根本上說,useMemouseCallback 是幫助我們優(yōu)化重新渲染的工具簸呈。它們通過兩種方式做到這一點:

    1. 減少在給定渲染中需要完成的工作量榕订。
    1. 減少組件需要重新渲染的次數。

我們一個一個來討論這些策略蜕便。

用例 1:繁重的計算

假設我們正在構建一個工具劫恒,幫助用戶找到所有介于 0 和 selectedNum(用戶提供的值)之間的素數。素數是指只能被 1 和它本身整除的數字轿腺,例如 17两嘴。

這是一個可能的實現:

import React from 'react';

function App() {
  // 我們將用戶選擇的數字保存在狀態(tài)中。
  const [selectedNum, setSelectedNum] = React.useState(100);
  
  // 計算 0 到用戶選擇的數字 `selectedNum` 之間的所有素數:
  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }
  
  return (
    <>
      <form>
        <label htmlFor="num">你的數字:</label>
        <input
          type="number"
          value={selectedNum}
          onChange={(event) => {
            // 為了防止計算機崩潰族壳,我們將最大值設置為 100k
            let num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        在 1 和 {selectedNum} 之間有 {allPrimes.length} 個素數:
        <span className="prime-list">
          {allPrimes.join(', ')}
        </span>
      </p>
    </>
  );
}

// 判斷給定數字是否為素數的輔助函數憔辫。
function isPrime(n) {
  const max = Math.ceil(Math.sqrt(n));
  
  if (n === 2) {
    return true;
  }
  
  for (let counter = 2; counter <= max; counter++) {
    if (n % counter === 0) {
      return false;
    }
  }

  return true;
}
export default App;

我不希望你逐行閱讀代碼,所以這里是關鍵點:

  • 我們有一個狀態(tài)變量 selectedNum仿荆,保存用戶選擇的數字贰您。
  • 使用 for 循環(huán),我們手動計算 0 到 selectedNum 之間的所有素數赖歌。
  • 渲染一個受控的數字輸入框枉圃,讓用戶可以更改 selectedNum
  • 展示我們計算出的所有素數庐冯。

這段代碼需要進行大量計算孽亲。如果用戶選擇了一個較大的 selectedNum,我們將需要遍歷數萬個數字展父,檢查它們是否是素數返劲。即使有更高效的素數檢查算法玲昧,這種計算始終會消耗大量資源。

我們確實需要在某些時候執(zhí)行這個計算篮绿,比如當用戶選擇了一個新的 selectedNum 時孵延。但如果我們不必要地重復這些工作,就可能會遇到性能問題亲配。

例如尘应,假設我們的示例還包含一個數字時鐘:

import React from 'react';
  const allPrimes = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      allPrimes.push(counter);
    }
  }
  
  return (
    <>
      <p className="clock">
        {format(time, 'hh:mm:ss a')}
      </p>
      <form>
        <label htmlFor="num">你的數字:</label>
        <input
          type="number"
          value={selectedNum}
          onChange={(event) => {
            let num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        在 1 和 {selectedNum} 之間有 {allPrimes.length} 個素數:
        <span className="prime-list">
          {allPrimes.join(', ')}
        </span>
      </p>
    </>
  );
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

function isPrime(n) {
  const max = Math.ceil(Math.sqrt(n));
  
  if (n === 2) {
    return true;
  }
  
  for (let counter = 2; counter <= max; counter++) {
    if (n % counter === 0) {
      return false;
    }
  }

  return true;
}

export default App;

我們的應用程序現在有兩個狀態(tài)變量:selectedNumtime。每秒吼虎,time 變量都會更新一次犬钢,以反映當前時間,并使用該值在右上角渲染一個數字時鐘思灰。

問題是:每當這兩個狀態(tài)變量中的任何一個發(fā)生變化時玷犹,我們都會重新執(zhí)行所有昂貴的素數計算。而且由于時間每秒都會變化洒疚,這意味著即使用戶沒有更改數字歹颓,我們也在不斷地重新生成素數列表!


在 JavaScript 中油湖,我們只有一個主線程巍扛,通過每秒反復運行這段代碼,使其非常繁忙肺魁。這意味著當用戶嘗試執(zhí)行其他操作時电湘,應用程序可能會感覺到緩慢,特別是在低端設備上鹅经。

但如果我們能“跳過”這些計算呢寂呛?如果我們已經擁有某個數字的素數列表,為什么不重復使用這個值瘾晃,而是每次都從頭開始計算贷痪?

這正是 useMemo 所允許我們做的。來看一下它的用法:

const allPrimes = React.useMemo(() => {
  const result = [];
  for (let counter = 2; counter < selectedNum; counter++) {
    if (isPrime(counter)) {
      result.push(counter);
    }
  }
  return result;
}, [selectedNum]);

useMemo 接受兩個參數:

  1. 一段需要執(zhí)行的工作蹦误,封裝在一個函數中劫拢。
  2. 一個依賴項列表。

在組件首次掛載時强胰,React 會調用這個函數來執(zhí)行所有邏輯舱沧,計算所有素數。函數的返回值會被賦給 allPrimes 變量偶洋。

對于后續(xù)的每次渲染熟吏,React 需要做一個決定:

  • 是重新調用該函數以重新計算值,還是
  • 復用上次計算的結果。

為了做出這個決定牵寺,React 會查看提供的依賴項列表悍引。依賴項自上次渲染以來是否發(fā)生了變化?如果是帽氓,React 將重新運行函數來計算新值趣斤。否則,它將跳過所有這些工作黎休,并復用之前計算的值浓领。

useMemo 本質上像一個小型緩存,依賴項則是緩存失效策略奋渔。

在這種情況下镊逝,我們的意思是:“僅當 selectedNum 發(fā)生變化時才重新計算素數列表〖稻ǎ”當組件由于其他原因重新渲染(例如 time 狀態(tài)變量的變化)時,useMemo 會忽略函數并傳遞緩存的值歹啼。

這種技術通常被稱為“記憶化”玄渗,這也是該鉤子被稱為 useMemo 的原因。

以下是該解決方案的一個在線版本:

import React from 'react';
import format from 'date-fns/format';

function App() {
  const [selectedNum, setSelectedNum] = React.useState(100);
  const time = useTime();
  
  const allPrimes = React.useMemo(() => {
    const result = [];
    
    for (let counter = 2; counter < selectedNum; counter++) {
      if (isPrime(counter)) {
        result.push(counter);
      }
    }
    
    return result;
  }, [selectedNum]);
  
  return (
    <>
      <p className="clock">
        {format(time, 'hh:mm:ss a')}
      </p>
      <form>
        <label htmlFor="num">你的數字:</label>
        <input
          type="number"
          value={selectedNum}
          onChange={(event) => {
            let num = Math.min(100_000, Number(event.target.value));
            setSelectedNum(num);
          }}
        />
      </form>
      <p>
        在 1 和 {selectedNum} 之間有 {allPrimes.length} 個素數:
        <span className="prime-list">
          {allPrimes.join(', ')}
        </span>
      </p>
    </>
  );
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

function isPrime(n) {
  const max = Math.ceil(Math.sqrt(n));
  
  if (n === 2) {
    return true;
  }
  
  for (let counter = 2; counter <= max; counter++) {
    if (n % counter === 0) {
      return false;
    }
  }

  return true;
}

export default App;
另一種方法

所以狸眼,useMemo 確實可以幫助我們避免不必要的計算……但它真的是這里的最佳解決方案嗎藤树?

我們經常可以通過重新組織應用程序的結構來避免使用 useMemo拓萌。

以下是我們可以做的一種方法:

import React from 'react';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

function App() {
  return (
    <>
      <Clock />
      <PrimeCalculator />
    </>
  );
}

export default App;

我將兩個新組件 ClockPrimeCalculator 提取出來岁钓。通過從 App 中分離出來,這兩個組件各自管理自己的狀態(tài)微王。一個組件的重新渲染不會影響另一個組件屡限。

這是顯示這種動態(tài)關系的圖表。每個框代表一個組件實例炕倘,當它們重新渲染時會閃爍钧大。可以嘗試點擊“Increment”按鈕來查看效果:


我們常常聽到“提升狀態(tài)”的說法罩旋,但有時候啊央,更好的方法是“下推狀態(tài)”! 每個組件應該有單一的責任,而在上述示例中涨醋,App 做了兩件完全無關的事情瓜饥。

當然,這并不總是一個選擇浴骂。在一個大型的真實應用程序中乓土,有許多狀態(tài)需要提升到較高的層級,不能被下推靠闭。

我在這種情況下還有另一個技巧帐我。

讓我們看一個例子坎炼。假設我們需要將 time 變量提升到 PrimeCalculator 之上:

import React from 'react';
import { getHours } from 'date-fns';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

// 將 PrimeCalculator 轉換為純組件:
const PurePrimeCalculator = React.memo(PrimeCalculator);

function App() {
  const time = useTime();

  // 根據時間選擇合適的背景色:
  const backgroundColor = getBackgroundColorFromTime(time);

  return (
    <div style={{ backgroundColor }}>
      <Clock time={time} />
      <PurePrimeCalculator />
    </div>
  );
}

const getBackgroundColorFromTime = (time) => {
  const hours = getHours(time);
  
  if (hours < 12) {
    // 早晨使用淺黃色
    return 'hsl(50deg 100% 90%)';
  } else if (hours < 18) {
    // 下午使用暗藍色
    return 'hsl(220deg 60% 92%)'
  } else {
    // 晚上使用深藍色
    return 'hsl(220deg 100% 80%)';
  }
}

function useTime() {
  const [time, setTime] = React.useState(new Date());
  
  React.useEffect(() => {
    const intervalId = window.setInterval(() => {
      setTime(new Date());
    }, 1000);
  
    return () => {
      window.clearInterval(intervalId);
    }
  }, []);
  
  return time;
}

更新圖表

這里是更新后的圖表,顯示發(fā)生了什么:


像一個力場一樣拦键,React.memo 將我們的組件包裹起來谣光,保護它不受無關更新的影響。我們的 PurePrimeCalculator 僅在接收到新數據或其內部狀態(tài)更改時重新渲染芬为。

這被稱為純組件萄金。基本上媚朦,我們是在告訴 React氧敢,這個組件在相同輸入下總會產生相同的輸出,我們可以跳過沒有變化的重新渲染询张。

我在最近的博文《為什么 React 會重新渲染》中詳細討論了 React.memo 的工作原理孙乖。

更傳統(tǒng)的方法
在上述示例中,我將 React.memo 應用到了導入的 PrimeCalculator 組件上份氧。
實際上唯袄,這有點不尋常。我選擇這樣結構化蜗帜,以便在同一個文件中看到所有內容恋拷,從而更容易理解。
在實際應用中厅缺,我通常將 React.memo 應用于組件導出蔬顾,如下所示:

// PrimeCalculator.js
function PrimeCalculator() {
 /* 組件邏輯 */
}
export default React.memo(PrimeCalculator);

現在我們的 PrimeCalculator 組件將始終是純的,而我們在使用它時不必進行調整湘捎。
如果我們需要非純版本的 PrimeCalculator诀豁,我們可以將底層組件作為命名導出。我認為我從未需要這樣做消痛。

這里有一個有趣的觀點轉變:之前我們是對特定計算的結果進行記憶化且叁,計算素數。然而在這個例子中秩伞,我對整個組件進行了記憶化逞带。

無論哪種方式,昂貴的計算工作只會在用戶選擇新的 selectedNum 時重新運行纱新。但我們優(yōu)化了父組件展氓,而不是特定的慢代碼行。

我并不是說一種方法比另一種更好脸爱;每種工具都有其在工具箱中的位置遇汞。但在這個特定的案例中,我更喜歡這種方法。

現在空入,如果你曾經嘗試在真實環(huán)境中使用純組件络它,你可能注意到一個奇特的現象:即使看起來沒有變化,純組件通常也會重新渲染很多次歪赢!??

這為我們引出了 useMemo 解決的第二個問題化戳。

更多替代方案
在他的博文“在你使用 memo() 之前”,Dan Abramov 分享了另一種基于使用子組件重構應用程序的方法埋凯,以避免需要進行任何記憶化点楼。
感謝 Yuval Shimoni 提出這一點!

使用案例 2:保留引用

在下面的示例中白对,我創(chuàng)建了一個 Boxes 組件掠廓。它顯示了一組多彩的盒子,供某種裝飾用途使用甩恼。

我還有一個不相關的狀態(tài)蟀瞧,即用戶的姓名。

import React from 'react';
import Boxes from './Boxes';

function App() {
  const [name, setName] = React.useState('');
  const [boxWidth, setBoxWidth] = React.useState(1);

  const id = React.useId();

  // 嘗試改變其中一些值媳拴!
  const boxes = [
    {
      flex: boxWidth,
      background: 'hsl(345deg 100% 50%)',
    },
    {
      flex: 3,
      background: 'hsl(260deg 100% 40%)',
    },
    {
      flex: 1,
      background: 'hsl(50deg 100% 60%)',
    },
  ];

  return (
    <>
      <Boxes boxes={boxes} />

      <section>
        <div className="row">
          <label htmlFor={`${id}-name`}>
            Name:
          </label>
          <input
            id={`${id}-name`}
            type="text"
            value={name}
            onChange={(event) => {
              setName(event.target.value);
            }}
          />
        </div>
        <label htmlFor={`${id}-box-width`}>
          First box width:
        </label>
        <input
          id={`${id}-box-width`}
          type="range"
          min={1}
          max={5}
          step={0.01}
          value={boxWidth}
          onChange={(event) => {
            setBoxWidth(Number(event.target.value));
          }}
        />
      </section>
    </>
  );
}

export default App;

Boxes 是一個純組件黄橘,因為它在 Boxes.js 中用 React.memo() 包裝的默認導出。這意味著它應該僅在其 props 發(fā)生變化時重新渲染屈溉。

然而,每當用戶更改他們的名字時抬探,Boxes 也會重新渲染子巾!

這里有一個圖表顯示了這種動態(tài)。嘗試在文本輸入框中輸入內容小压,注意兩個組件是如何重新渲染的:

777.png

這是怎么回事线梗?為什么我們的 React.memo() 在這里沒有保護我們?

Boxes 組件只有一個 prop:boxes怠益,并且看起來我們在每次渲染時都給它提供了相同的數據仪搔。它總是相同的:一個紅色的盒子,一個寬寬的紫色盒子蜻牢,一個黃色的盒子烤咧。我們確實有一個影響 boxes 數組的 boxWidth 狀態(tài)變量,但我們并沒有改變它抢呆!

問題在于:每次 React 重新渲染時煮嫌,我們都在生成一個全新的數組。它們在值上是等價的抱虐,但在引用上卻不是昌阿。

我認為如果我們先忘掉 React,聊聊普通的 JavaScript 會有幫助。讓我們看一個類似的情況:

function getNumbers() {
  return [1, 2, 3];
}
const firstResult = getNumbers();
const secondResult = getNumbers();
console.log(firstResult === secondResult);

你覺得呢懦冰?firstResult 是否等于 secondResult灶轰?

從某種意義上說,它們是相等的刷钢。兩個變量持有相同的結構 [1, 2, 3]笋颤。但這并不是 === 運算符實際上在檢查的內容。

相反闯捎,=== 正在檢查兩個表達式是否是同一個東西椰弊。

我們創(chuàng)建了兩個不同的數組。它們可能包含相同的內容瓤鼻,但它們不是同一個數組秉版,就像兩個相同的雙胞胎不是同一個人一樣。


每次我們調用 getNumbers 函數時茬祷,我們都會創(chuàng)建一個全新的數組清焕,計算機內存中持有的獨特事物。如果我們多次調用它祭犯,我們將在內存中存儲多個該數組的副本秸妥。

注意,簡單數據類型(如字符串沃粗、數字和布爾值)可以通過值進行比較粥惧。但對于數組和對象,它們只能通過引用進行比較最盅。有關這種區(qū)別的更多信息突雪,請查看 Dave Ceddia 的精彩博文:JavaScript 中引用的視覺指南

回到 React: 我們的 Boxes React 組件也是一個 JavaScript 函數涡贱。當我們渲染它時咏删,我們調用該函數:

// 每次我們渲染這個組件時,我們調用這個函數...
function App() {
  // ...并生成一個全新的數組...
  const boxes = [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
  // ...然后將其作為 prop 傳遞給該組件问词!
  return (
    <Boxes boxes={boxes} />
  );
}

name 狀態(tài)改變時督函,我們的 App 組件重新渲染,這會重新運行所有代碼激挪。我們構建了一個全新的 boxes 數組辰狡,并將其傳遞給 Boxes 組件。

Boxes 會重新渲染灌灾,因為我們給了它一個全新的數組搓译!

盒子數組的結構在渲染之間沒有改變,但這并不相關锋喜。React 所知道的就是 boxes prop 收到了一個新創(chuàng)建的些己、前所未見的數組豌鸡。

為了解決這個問題,我們可以使用 useMemo 鉤子:

const boxes = React.useMemo(() => {
  return [
    { flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
    { flex: 3, background: 'hsl(260deg 100% 40%)' },
    { flex: 1, background: 'hsl(50deg 100% 60%)' },
  ];
}, [boxWidth]);

與之前看到的素數示例不同段标,我們在這里并不擔心計算開銷涯冠。我們的唯一目標是保留對特定數組的引用。

我們將 boxWidth 列為依賴逼庞,因為我們希望 Boxes 組件在用戶調整紅色盒子的寬度時重新渲染蛇更。

我認為一個快速的草圖將有助于說明。在之前的情況下赛糟,我們每次都會創(chuàng)建一個全新的數組派任,作為每個快照的一部分:


然而,通過 useMemo璧南,我們可以重用先前創(chuàng)建的 boxes 數組:

通過在多個渲染中保留相同的引用掌逛,我們允許純組件按我們希望的方式運行,忽略不影響用戶界面的渲染司倚。

useCallback 鉤子

好吧豆混,那么我們已經涵蓋了 useMemo……那 useCallback 呢?

簡而言之:它與 useMemo 完全相同动知,但用于函數而不是數組/對象皿伺。

與數組和對象類似,函數也是通過引用進行比較盒粮,而不是通過值:

const functionOne = function() {
  return 5;
};
const functionTwo = function() {
  return 5;
};
console.log(functionOne === functionTwo); // false

這意味著鸵鸥,如果我們在組件內部定義一個函數,它將在每次渲染時重新生成丹皱,產生一個相同但唯一的函數脂男。

讓我們看一個例子:

import React from 'react';
import MegaBoost from './MegaBoost';

function App() {
  const [count, setCount] = React.useState(0);

  function handleMegaBoost() {
    setCount((currentValue) => currentValue + 1234);
  }

  return (
    <>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
      <MegaBoost handleClick={handleMegaBoost} />
    </>
  );
}

export default App;

這個沙盒描繪了一個典型的計數器應用程序,但有一個特殊的“Mega Boost”按鈕种呐。這個按鈕可以大幅增加計數,以防你趕時間弃甥,不想多次點擊標準按鈕爽室。

MegaBoost 組件是一個純組件,得益于 React.memo淆攻。它不依賴于 count……但每當 count 改變時阔墩,它都會重新渲染!

和我們看到的 boxes 數組一樣瓶珊,這里問題在于我們在每次渲染時都在生成一個全新的函數啸箫。如果我們渲染 3 次,我們將創(chuàng)建 3 個獨立的 handleMegaBoost 函數伞芹,突破 React.memo 的保護忘苛。

使用我們關于 useMemo 學到的知識蝉娜,我們可以這樣解決問題:

const handleMegaBoost = React.useMemo(() => {
  return function() {
    setCount((currentValue) => currentValue + 1234);
  }
}, []);

我們不是返回一個數組,而是返回一個函數。這個函數然后被存儲在 handleMegaBoost 變量中。

這樣可以工作……但還有更好的方法:

const handleMegaBoost = React.useCallback(() => {
  setCount((currentValue) => currentValue + 1234);
}, []);

useCallbackuseMemo 的作用相同线婚,但專門為函數構建辕狰。我們直接將一個函數傳遞給它,它會將該函數進行記憶蛹尝,以便在渲染之間進行傳遞。

換句話說,這兩個表達式具有相同的效果:

// 這個:
React.useCallback(function helloWorld(){}, []);
// ...在功能上等同于這個:
React.useMemo(() => function helloWorld(){}, []);

useCallback 是語法糖倍阐。它純粹是為了在嘗試記憶回調函數時讓我們的生活變得更加輕松。

何時使用這些鉤子

好吧逗威,我們已經看到 useMemouseCallback 如何允許我們在多個渲染中傳遞引用峰搪,以重用復雜的計算或避免打破純組件。問題是:我們應該多頻繁使用它庵楷?

在我個人看來罢艾,把每一個對象/數組/函數都包裹在這些鉤子中是浪費時間。大多數情況下尽纽,收益微不足道咐蚯;React 的優(yōu)化非常好,重新渲染往往并沒有我們想象的那么慢或昂貴弄贿!

使用這些鉤子的最佳方法是針對具體問題春锋。如果你注意到你的應用變得有點遲緩,可以使用 React Profiler 來查找緩慢的渲染差凹。在某些情況下期奔,你能夠通過重構應用來提高性能。在其他情況下危尿,useMemouseCallback 可以幫助加速呐萌。

(如果你不確定如何使用 React Profiler,我在最近的博客文章《為什么 React 會重新渲染》中進行了詳細介紹R杲俊)

盡管如此肺孤,有幾個場景我確實會預先應用這些鉤子。

這可能會在未來發(fā)生變化济欢!
React 團隊正在積極調查在編譯階段是否可能“自動記憶”代碼赠堵。這仍處于研究階段,但早期實驗結果似乎很有希望法褥。
在未來茫叭,所有這些事情都可能會自動為我們完成。不過在那之前半等,我們仍然需要自己進行優(yōu)化揍愁。
有關更多信息呐萨,請查看 Xuan Huang 的講座《沒有記憶的 React》

通用自定義鉤子內部

我最喜歡的小自定義鉤子之一是 useToggle吗垮,這是一個友好的助手垛吗,幾乎與 useState 完全相同,但只能在 truefalse 之間切換狀態(tài)變量:

function App() {
  const [isDarkMode, toggleDarkMode] = useToggle(false);
  return (
    <button onClick={toggleDarkMode}>
      Toggle color theme
    </button>
  );
}

這是如何定義這個自定義鉤子的:

function useToggle(initialValue) {
  const [value, setValue] = React.useState(initialValue);
  const toggle = React.useCallback(() => {
    setValue(v => !v);
  }, []);
  return [value, toggle];
}

注意 toggle 函數是用 useCallback 進行記憶的烁登。

當我構建像這樣的自定義可重用鉤子時怯屉,我喜歡盡可能使它們高效,因為我不知道它們將來會在哪里被使用饵沧。雖然在 95% 的情況下可能是過度設計锨络,但如果我使用這個鉤子 30 或 40 次,這很可能會幫助提高我的應用性能狼牺。

在上下文提供者內部

當我們使用上下文在應用程序中共享數據時羡儿,通常會將一個大對象作為 value 屬性傳遞。

通常是钥,記憶化這個對象是個好主意:

const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }) {
  const memoizedValue = React.useMemo(() => {
    return {
      user,
      status,
      forgotPwLink,
    };
  }, [user, status, forgotPwLink]);
  
  return (
    <AuthContext.Provider value={memoizedValue}>
      {children}
    </AuthContext.Provider>
  );
}

為什么這樣做是有益的掠归?:可能有很多純組件會消費這個上下文。如果不使用 useMemo悄泥,那么當 AuthProvider 的父組件重新渲染時虏冻,所有這些組件都將被迫重新渲染。

React 的樂趣

呼弹囚!你終于到了最后厨相。我知道這個教程涉及了一些相當復雜的內容。??

我知道這兩個鉤子很棘手鸥鹉,React 本身可能會讓人感到非常壓倒和困惑蛮穿。這是一個困難的工具!

但問題是:如果你能克服最初的障礙毁渗,使用 React 將是一種絕對的樂趣践磅。

我從 2015 年開始使用 React,它已成為我構建復雜用戶界面和 web 應用程序的絕對最愛灸异。我嘗試過幾乎所有的 JS 框架音诈,但我在使用它們時的生產力不如使用 React 時高。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末绎狭,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子褥傍,更是在濱河造成了極大的恐慌儡嘶,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恍风,死亡現場離奇詭異蹦狂,居然都是意外死亡誓篱,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門凯楔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窜骄,“玉大人,你說我怎么就攤上這事摆屯×诙簦” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵虐骑,是天一觀的道長准验。 經常有香客問我,道長廷没,這世上最難降的妖魔是什么糊饱? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮颠黎,結果婚禮上另锋,老公的妹妹穿的比我還像新娘。我一直安慰自己狭归,他們只是感情好夭坪,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著唉铜,像睡著了一般台舱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上潭流,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天竞惋,我揣著相機與錄音,去河邊找鬼灰嫉。 笑死拆宛,一個胖子當著我的面吹牛,可吹牛的內容都是我干的讼撒。 我是一名探鬼主播浑厚,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼根盒!你這毒婦竟也來了钳幅?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤炎滞,失蹤者是張志新(化名)和其女友劉穎敢艰,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體册赛,經...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钠导,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年震嫉,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牡属。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡票堵,死狀恐怖,靈堂內的尸體忽然破棺而出逮栅,到底是詐尸還是另有隱情悴势,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布证芭,位于F島的核電站瞳浦,受9級特大地震影響,放射性物質發(fā)生泄漏废士。R本人自食惡果不足惜叫潦,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望官硝。 院中可真熱鬧矗蕊,春花似錦、人聲如沸氢架。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岖研。三九已至卿操,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孙援,已是汗流浹背害淤。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拓售,地道東北人窥摄。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像础淤,于是被迫代替她去往敵國和親崭放。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內容