重讀 React 官方文檔

前言

大家好耻陕,我是麥西。

近來發(fā)現(xiàn) React 官方文檔更新了夷家。

仔細想來绒疗,學(xué)習(xí)使用 React 這么久還沒有好好拜讀過官方文檔。于是認真讀寫了一遍官方教程腺毫。這里把學(xué)到的一些知識記錄下來癣疟,分享給大家。

純函數(shù)組件

React 官方推薦全面擁抱 hooks潮酒。這也就意味著睛挚,類組件已經(jīng)是過去式了。這一點從官方文檔也可以看出急黎,新的官方文檔已經(jīng)不再介紹和使用類組件了扎狱。

部分 JavaScript 函數(shù)是存粹的,這類函數(shù)被稱為純函數(shù)勃教。

純函數(shù)通常具有以下特征:

  • 只負責(zé)自己的工作淤击。它不會更改函數(shù)調(diào)用前就存在的對象或變量。
  • 輸入相同故源,則輸出相同遭贸。給定相同的輸入,純函數(shù)總是返回相同的結(jié)果心软。

可簡單的理解為壕吹,函數(shù)的執(zhí)行不依賴且不改變外界。純函數(shù)的優(yōu)點是沒有副作用删铃,可移植性好耳贬。在 A 項目能夠用,B 項目想要使用直接拿過來就好了猎唁≈渚ⅲ可以通過下面這幾個例子感受下純函數(shù)的概念:

// 純函數(shù)
function add(a, b) {
    return a + b;
}

// 非純函數(shù),函數(shù)執(zhí)行依賴外界變量all
let all = 100;
function plus(a) {
    return all + a;
}

// 非純函數(shù),函數(shù)執(zhí)行改變了外界變量obj
let obj = {};
function fun(a) {
    obj.a = a;
    return a;
}

// 非純函數(shù)腐魂,函數(shù)的執(zhí)行依賴外界getCount()
async function(a, b) {
  const c = await getCount(); // 副作用
  return a + b +c;
}

// addConst是否是純函數(shù)存在爭議帐偎,我更傾向于它是
const data = 100;
function addConst(a) {
  return a + data;
}

最后一個例子,addConst 依賴于 data, 但 data 是常量蛔屹。這種情況存在爭議削樊,有人認為是,也有人認為不是兔毒。我更傾向于addConst是純函數(shù)漫贞。

官方建議建議我們使用純函數(shù)來編寫組件。非純函數(shù)編寫的組件可能會存在副作用育叁,造成意料之外的影響迅脐。下面是一個非純函數(shù)組件的例子:

let guest = 0;

function Cup() {
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

頁面顯示的結(jié)果是:

Tea cup for guest #1

Tea cup for guest #2

Tea cup for guest #3

在上面這個例子中,Cup 是非純函數(shù)組件豪嗽,它依賴于外界 guest 變量谴蔑。由于多個 Cup 組件依賴的是同一個變量guest。當(dāng)我們每次使用組件的時候龟梦,都會修改guest树碱,這就會導(dǎo)致每次使用組件都會產(chǎn)生不同的結(jié)果。

因此变秦,為了避免出現(xiàn)意想不到的結(jié)果成榜,我們最好使用純函數(shù)編寫組件

渲染和提交

在 React 應(yīng)用中一次屏幕更新都會發(fā)生以下三個步驟:

1. 觸發(fā)

也就說觸發(fā)一次渲染蹦玫。有兩種原因會導(dǎo)致組件渲染:

  • 組件的初次渲染: 當(dāng)應(yīng)用啟動時赎婚,會觸發(fā)初次渲染。也就是 render 方法的執(zhí)行樱溉。
import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<Image />); // 初次渲染
  • 組件或者其祖先的狀態(tài)發(fā)生了改變

一旦組件被初始渲染后挣输,我們可以通過 set函數(shù)更新組件狀態(tài)來觸發(fā)之后的渲染。

2. 渲染

在我們觸發(fā)渲染后福贞,React 會調(diào)用組件來確定要在屏幕上顯示的內(nèi)容撩嚼。渲染中 即 React 在調(diào)用你的組件函數(shù)。

  • 在進行初次渲染時, React 會調(diào)用根組件挖帘。

  • 對于后續(xù)的渲染, React 會調(diào)用 內(nèi)部狀態(tài)更新 觸發(fā)了渲染 的函數(shù)組件完丽。

3. 提交

在渲染(調(diào)用)您的組件之后,React 將會修改 DOM拇舀。

  • 對于初次渲染逻族, React 會使用 appendChild() DOM API 將其創(chuàng)建的所有 DOM 節(jié)點放在屏幕上。

  • 對于再次渲染骄崩, React 將應(yīng)用最少的必要操作(在渲染時計算)聘鳞,以使得 DOM 與最新的渲染輸出相互匹配薄辅。

    React 僅在渲染之間存在差異時才會更改 DOM 節(jié)點。 如果渲染結(jié)果與上次一樣抠璃,那么 React 將不會修改 DOM站楚。

在渲染完成并且 React 更新 DOM 之后,瀏覽器就會重新繪制屏幕搏嗡。

useState

使用 state 需要注意以下幾點:

  • 當(dāng)一個組件需要在多次渲染記住某些信息時窿春,使用 state 變量。

  • 調(diào)用 Hook 時彻况,包括 useState谁尸,僅在組件或者另一個 Hook 的頂層作用域調(diào)用舅踪。

  • state 是隔離且私有的纽甘。也就是說,將一個組件調(diào)用兩次抽碌,他們內(nèi)部的 state 不會互相影響悍赢。

1. state 如同一張快照

當(dāng) React 重新渲染一個組件時:

  1. React 會再次調(diào)用你的函數(shù)
  2. 你的函數(shù)會返回新的 JSX
  3. React 會更新界面來匹配你返回的 JSX

作為一個組件的記憶,state 不同于在你的函數(shù)返回之后就會消失的普通變量货徙。state 實際上“活”在 React 本身中——就像被擺在一個架子上左权!——位于你的函數(shù)之外。當(dāng) React 調(diào)用你的組件時痴颊,它會為特定的那一次渲染提供一張 state 快照赏迟。你的組件會在其 JSX 中返回一張包含一整套新的 props 和事件處理函數(shù)的 UI 快照 ,其中所有的值都是 根據(jù)那一次渲染中 state 的值 被計算出來的蠢棱!

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1);
          setNumber(number + 1);
          setNumber(number + 1);
        }}>
        +3
      </button>
    </>
  );
}

以下是這個按鈕的點擊事件處理函數(shù)通知 React 要做的事情:

  1. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)锌杀。
    React 準(zhǔn)備在下一次渲染時將 number 更改為 1。
  2. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)泻仙。
    React 準(zhǔn)備在下一次渲染時將 number 更改為 1糕再。
  3. setNumber(number + 1)number 是 0 所以 setNumber(0 + 1)
    React 準(zhǔn)備在下一次渲染時將 number 更改為 1玉转。

盡管你調(diào)用了三次 setNumber(number + 1)突想,但在這次渲染的 事件處理函數(shù)中 number 會一直是 0,所以你會三次將 state 設(shè)置成 1究抓。這就是為什么在你的事件處理函數(shù)執(zhí)行完以后猾担,React 重新渲染的組件中的 number 等于 1 而不是 3。

為了更好理解刺下,我們看下面這個例子:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 5);
          setTimeout(() => {
            alert(number);
          }, 3000);
        }}>
        +5
      </button>
    </>
  );
}

點擊+5 后垒探,彈出的數(shù)字是 0,而不是 5. 點擊按鈕后的操作:

  1. setNumber(0+5)
  2. js setTimeout(() => { alert(0) })

2. 將 state 加入隊列

React 會對 state 更新進行批處理怠李。在上面的示例中圾叼,連續(xù)調(diào)用了三次setNumber(number + 1)并不能得到我們想要的結(jié)果蛤克。

React 會等到事件處理函數(shù)中的所有代碼都運行完畢再處理你的 state 更新。

我們可以通過更新函數(shù)來在下次渲染之前多次更新同一個 state夷蚊。比如:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber((n) => n + 1);
          setNumber((n) => n + 1);
          setNumber((n) => n + 1);
        }}>
        +3
      </button>
    </>
  );
}

下面是 React 在執(zhí)行事件處理函數(shù)時處理這幾行代碼的過程:

  1. setNumber(n => n + 1)n => n + 1 是一個函數(shù)构挤。React 將它加入隊列。
  2. setNumber(n => n + 1)n => n + 1 是一個函數(shù)惕鼓。React 將它加入隊列筋现。
  3. setNumber(n => n + 1)n => n + 1 是一個函數(shù)。React 將它加入隊列箱歧。

當(dāng)你在下次渲染期間調(diào)用 useState 時矾飞,React 會遍歷隊列。之前的 number state 的值是 0呀邢,所以這就是 React 作為參數(shù) n 傳遞給第一個更新函數(shù)的值洒沦。然后 React 會獲取你上一個更新函數(shù)的返回值,并將其作為 n 傳遞給下一個更新函數(shù)价淌,以此類推:

更新隊列 n 返回值
n => n + 1 0 0 + 1 = 1
n => n + 1 1 1 + 1 = 2
n => n + 1 2 2 + 1 = 3

看看下面這個例子:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 5);
          setNumber((n) => n + 1);
          setNumber(42);
        }}>
        增加數(shù)字
      </button>
    </>
  );
}

以下是 React 在執(zhí)行事件處理函數(shù)時處理這幾行代碼的過程:

  1. setNumber(number + 5)number 為 0申眼,所以 setNumber(0 + 5)。React 將 “替換為 5” 添加到其隊列中蝉衣。
  2. setNumber(n => n + 1)n => n + 1 是一個更新函數(shù)括尸。React 將該函數(shù)添加到其隊列中。
  3. setNumber(42):React 將 “替換為 42” 添加到其隊列中病毡。

在下一次渲染期間濒翻,React 會遍歷 state 隊列:

更新隊列 n 返回值
替換為 5 0 5
n => n + 1 5 5 + 1 = 6
替換為 42 6 42

可以這樣來理解狀態(tài)隊列的更新:

function getFinalState(baseState, queue) {
  let finalState = baseState;
  queue.forEach((update) => {
    finalState = typeof update === 'function' ? update(finalState) : update;
  });
  return finalState;
}

其中 baseState 是初始狀態(tài),queue 是狀態(tài)更新隊列啦膜,包括數(shù)據(jù)和更新函數(shù)有送。

3. set 函數(shù)一定會觸發(fā)更新嗎?

看下面這個例子:

export default function Counter() {
  const [number, setNumber] = useState(0);
  const [person, setPerson] = useState({ name: 'jack' });
  console.log('渲染');

  return (
    <>
      <button
        onClick={() => {
          setNumber(number);
        }}>
        增加數(shù)字
      </button>
      <h1>{number}</h1>
      <button
        onClick={() => {
          person.age = 18;
          setPerson(person);
        }}>
        修改對象
      </button>
      <h1>{JSON.stringify(person)}</h1>
    </>
  );
}

組件的更新意味著組件函數(shù)的重新執(zhí)行功戚。對于上面這個例子娶眷,無論是點擊 增加數(shù)字 還是 改變對象 都沒有打印 渲染

set 函數(shù)觸發(fā)更新的條件:

  • 值類型啸臀,state 的值改變
  • 引用類型届宠,state 的引用改變

對于上面的例子:

  • number 是值類型。點擊增加數(shù)字乘粒,值沒有改變豌注,不會觸發(fā)更新。
  • person 是引用類型灯萍。點擊修改對象轧铁,雖然 person 對象的值雖然變化了,但是引用地址沒有變化旦棉,因此也不會觸發(fā)更新齿风。

4. 構(gòu)建 state 的原則

  1. 合并關(guān)聯(lián)的 state

有時候我們可能會不確定使用單個 state 還是多個 state 變量药薯。

const [x, setX] = useState(0);
const [y, setY] = useState(0);

const [position, setPosition] = useState({ x: 0, y: 0 });

從技術(shù)上講,我們可以使用其中任何一種方法救斑。但是童本,如果某兩個 state 變量總是一起變化,則將它們統(tǒng)一成一個 state 變量可能更好脸候。這樣你就不會忘記讓它們始終保持同步穷娱。

  1. 避免矛盾的 state
import { useState } from 'react';

export default function Send() {
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          setIsSent(false);
          setIsSending(true);
          setTimeout(() => {
            setIsSending(false);
            setIsSent(true);
          }, 2000);
        }}>
        發(fā)送
      </button>
      {isSending && <h1>正在發(fā)送...</h1>}
      {isSent && <h1>發(fā)送完成</h1>}
    </>
  );
}

盡管這段代碼是有效的,但也會讓一些 state “極難處理”运沦。例如泵额,如果你忘記同時調(diào)用 setIsSentsetIsSending,則可能會出現(xiàn) 二者 同時為 true 的情況携添。

可以用一個 status 變量來代替它們嫁盲。代碼如下:

import { useState } from 'react';

export default function Send() {
  const [status, setStatus] = useState('init');
  return (
    <>
      <button
        onClick={() => {
          setStatus('sending');
          setTimeout(() => {
            setStatus('sent');
          }, 2000);
        }}>
        發(fā)送
      </button>
      {status === 'sending' && <h1>正在發(fā)送...</h1>}
      {status === 'sent' && <h1>發(fā)送完成</h1>}
    </>
  );
}
  1. 避免冗余的 state
import { useState } from 'react';

export default function Name() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  return (
    <>
      <span>First Name</span>
      <input
        value={firstName}
        onChange={(e) => {
          setFirstName(e.target.value);
          setFullName(e.target.value + ' ' + lastName);
        }}
      />
      <span>Last Name</span>
      <input
        value={lastName}
        onChange={(e) => {
          setLastName(e.target.value);
          setFullName(firstName + ' ' + e.target.value);
        }}
      />
      <h1>{fullName}</h1>
    </>
  );
}

能夠看出,fullName 是冗余的 state薪寓。我們可以直接:

const fullName = firstName + ' ' + lastName;

無需再把 fullName 存放到 state 中亡资。

  1. 避免重復(fù)的 state

有時候澜共,在我們存儲的 state 中向叉,可能有兩個 state 有重合的部分。這時候我們就要考慮是不是有重復(fù)的問題了嗦董。

具體例子見這里

5. 保存和重置 state

前面我們說過母谎,組件內(nèi)部的 state 是互相隔離的。一個組件 state 的改變不會影響另外一個京革。然而奇唤,我們看下面這個例子

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
      <label>
        <input
          type='checkbox'
          checked={isFancy}
          onChange={(e) => {
            setIsFancy(e.target.checked);
          }}
        />
        使用好看的樣式
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)}>
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>加一</button>
    </div>
  );
}

當(dāng)我們修改了 Counter組件 的 state 后,點擊 checkbox 切換到另一個 Counter匹摇,舊 Counter 的 state 并沒有變?yōu)?0咬扇,而是保留了下來。如下圖:

在 React 中廊勃,相同位置的相同組件會使得 state 保留下來懈贺。那么怎么才能讓上述例子的 state 重置呢?

有兩種方法:

1. 將組件渲染在不同的位置

{
  isFancy ? (
    <Counter isFancy={true} />
  ) : (
    <div>
      <Counter isFancy={false} />
    </div>
  );
}

2. 使用 key 來標(biāo)識組件

{
  isFancy ? <Counter isFancy={true} key={fancyTrue} /> : <Counter isFancy={false} key={fancyFalse} />;
}

效果如下:

useRef

基本用法

提起 useRef坡垫,很多人都會把它跟 DOM 聯(lián)系起來梭灿。其實 useRef 不止可以用來存儲 DOM 元素。它的定義是:

如果希望組件記住某些信息冰悠,但又不想讓這些信息觸發(fā)新的渲染堡妒,可以使用 useRef。

比如下面這個例子

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

let timer = null;

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

  const onStart = () => {
    timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  };

  const onStop = () => {
    clearInterval(timer);
  };

  useEffect(() => {
    return () => {
      clearInterval(timer);
    };
  }, []);

  return (
    <>
      <h1>{count}</h1>
      <button onClick={onStart}>開始</button>
      <button onClick={onStop}>停止</button>
    </>
  );
}

這個例子里溉卓,我們寫了一個 Counter 組件皮迟。點擊開始按鈕 count 每秒增加 1搬泥,點擊停止按鈕 count 停止增加。

看上去伏尼,這個組件好像很 OK佑钾。

但是如果在一個頁面使用 Counter 組件兩次,就會發(fā)現(xiàn)烦粒,第一個定時器停止不了休溶。

export default function App() {
  return (
    <div className='App'>
      <Counter />
      <Counter />
    </div>
  );
}

如下圖:

這是因為兩個組件公用同一個 timer 變量,第二個組件修改 timer 后扰她,導(dǎo)致第一個組件中 clearInterval 處理的是第二個組件的 timer兽掰。 因此第一個組件無法停止定時增加。

官網(wǎng)推薦我們使用純函數(shù)編寫組件也是基于此徒役。

估計有人會說孽尽,我可以把 timer 變量放到 Counter 內(nèi)部。組件內(nèi)部的變量是互相隔離的, 這樣就不會把第一個 Counter 組件的 timer 給覆蓋了忧勿。

放到內(nèi)部有兩種情況:

1. 直接使用變量杉女。 當(dāng)組件更新的時候,組件函數(shù)重新執(zhí)行鸳吸,會導(dǎo)致 timer 重新創(chuàng)建熏挎,因此并不能清除之前的 timer

2. 使用 state晌砾。 使用 state 可以解決問題坎拐,但是會導(dǎo)致不必要的渲染。每次 timer 變化都會導(dǎo)致組件重新渲染养匈。

其實對于這種定時器清理的問題哼勇,我們可以使用 useRef。useRef 創(chuàng)建一個變量呕乎,變量里有一個 current 屬性积担。

const timeRef = useRef(null);

比如上面這段代碼,會創(chuàng)建一個變量 timeRef, 它的結(jié)構(gòu)類似 { current: null }猬仁。

使用 ref 修改上述例子中的代碼:

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

  const onStart = () => {
    timeRef.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  };

  const onStop = () => {
    clearInterval(timeRef.current);
  };

  useEffect(() => {
    return () => {
      clearInterval(timeRef.current);
    };
  }, []);

  return (
    <>
      <h1>{count}</h1>
      <button onClick={onStart}>開始</button>
      <button onClick={onStop}>停止</button>
    </>
  );
}

運行試試帝璧,完美解決了之前的問題。

可以這樣理解逐虚,ref 跟 state 的區(qū)別是聋溜,ref 不會導(dǎo)致組件重新渲染。

使用 ref 操作 DOM

我們來看一個 ref 操作 DOM 的例子

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>聚焦輸入框</button>
    </>
  );
}

效果:點擊聚焦輸入框按鈕叭爱,輸入框?qū)劢埂?/p>

這段代碼主要做了以下事情:

  1. 使用 useRef Hook 聲明 inputRef撮躁。
  2. <input ref={inputRef}> 告訴 React 將這個 input 的 DOM 節(jié)點放入 inputRef.current
  3. handleClick 函數(shù)中买雾,從 inputRef.current 讀取 input DOM 節(jié)點并調(diào)用它的 focus()把曼。
  4. 給按鈕添加點擊事件handleClick

forwardRef

我們可以使用 ref 屬性配合 useRef 直接調(diào)用 DOM杨帽。那么可不可以給組件添加 ref 調(diào)用組件的 DOM 呢?讓我們來試一下

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦輸入框</button>
    </>
  );
}

我們給 MyInput 組件加上了 ref嗤军,可是當(dāng)我們點擊 聚焦輸入框按鈕注盈,則會報錯:Cannot read properties of null (reading 'focus')

也就是說 inputRef.currentnull。我們并不能拿到組件的 DOM 元素叙赚。

默認情況下老客,React 不允許組件訪問其他組件的 DOM 節(jié)點。這是因為 ref 是應(yīng)急方案震叮,應(yīng)當(dāng)謹(jǐn)慎使用胧砰。如果組件想要暴露自己的的 DOM,則需要使用forwardRef來包裝苇瓣,并把 ref 轉(zhuǎn)發(fā)給自己的子元素尉间。 比如這樣:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦輸入框</button>
    </>
  );
}

它是這樣工作的:

  1. <MyInput ref={inputRef} /> 告訴 React 將對應(yīng)的 DOM 節(jié)點放入 inputRef.current 中。但是击罪,這取決于 MyInput 組件是否允許這種行為哲嘲, 默認情況下是不允許的。

  2. MyInput 組件是使用 forwardRef 聲明的媳禁。 這讓從上面接收的 inputRef 作為第二個參數(shù) ref 傳入組件眠副,第一個參數(shù)是 props 。

  3. MyInput 組件將自己接收到的 ref 傳遞給它內(nèi)部的 <input>损话。

這樣就通過 forwardRef 向父組件暴露了子組件的 DOM 節(jié)點侦啸。

useEffect

useEffect 是使用頻率僅低于 useState 的 hook槽唾。很多人把 useEffect 當(dāng)做監(jiān)聽器來使用丧枪。這是不太妥當(dāng)?shù)摹?/p>

useEffect 是用來處理由渲染本身而不是點擊事件引起的副作用

基本用法

useEffect(setup, dependencies?)

  • setup 處理邏輯庞萍,是一個函數(shù)拧烦。可以返回一個清理函數(shù)

  • dependencies 是依賴項钝计,當(dāng)依賴項變化會執(zhí)行, 會執(zhí)行setup函數(shù)

值得注意的幾個問題

1. useEffect 的執(zhí)行時機

  • useEffect 在組件掛載完成后恋博,也就是說 DOM 更新完畢后,按照定義的順序執(zhí)行私恬。

比如這個例子:

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

export default function App() {
  const [number, setNumber] = useState(0);

  useEffect(() => {
    console.log('-- 空依賴1债沮,useEffect執(zhí)行 --');
  }, []);

  useEffect(() => {
    console.log('-- 非空依賴,useEffect執(zhí)行 --', number);
  }, [number]);

  useEffect(() => {
    console.log('-- 空依賴2本鸣,useEffect執(zhí)行 --');
  }, []);

  console.log('渲染');

  return (
    <>
      <h1>{number}</h1>
    </>
  );
}

結(jié)果打印為:

渲染
-- 空依賴1疫衩,useEffect執(zhí)行 --
-- 非空依賴,useEffect執(zhí)行 -- 0
-- 空依賴2荣德,useEffect執(zhí)行 --

執(zhí)行順序依次為:

所有 DOM 更新完畢 => 空依賴 1 useEffect 執(zhí)行 => 非空依賴 useEffect 執(zhí)行 => 空依賴 2 useEffect 執(zhí)行

  • useEffect 的清理函數(shù)在組件卸載期間調(diào)用或者下次運行之前調(diào)用闷煤。 比如下面這個例子:
import React, { useState, useEffect } from 'react';

function Title() {
  const [title, setTitle] = useState('這里是標(biāo)題');

  useEffect(() => {
    console.log('空依賴童芹,useEffect執(zhí)行');
    return () => console.log('空依賴,useEffect清理函數(shù)執(zhí)行');
  }, []);

  useEffect(() => {
    console.log('非空依賴鲤拿,useEffect執(zhí)行');
    return () => console.log('非空依賴假褪,useEffect清理函數(shù)執(zhí)行');
  }, [title]);

  return <h1 onClick={() => setTitle((title) => `${title}1`)}>{title}</h1>;
}

export default function App() {
  const [titleVisible, setTitleVisible] = useState(true);

  return (
    <>
      {titleVisible && <Title />}
      <button onClick={() => setTitleVisible(!titleVisible)}>{`${titleVisible ? '隱藏' : '顯示'}標(biāo)題`}</button>
    </>
  );
}

由前面我們知道,組件掛載完成后才會按照順序執(zhí)行 useEffect, 因此打印結(jié)果是:

空依賴近顷,useEffect執(zhí)行
非空依賴生音,useEffect執(zhí)行

然后點擊標(biāo)題,會打又仙:

非空依賴久锥,useEffect清理函數(shù)執(zhí)行
非空依賴,useEffect執(zhí)行

最后异剥,我們點擊 隱藏標(biāo)題 按鈕瑟由,會打印:

空依賴冤寿,useEffect清理函數(shù)執(zhí)行
非空依賴歹苦,useEffect清理函數(shù)執(zhí)行

也就是說,空依賴的 useEffect 只會在組件掛載完成后執(zhí)行督怜,清理函數(shù)只會在組件卸載后執(zhí)行

非空依賴的 useEffect 則有兩種情況:

  1. 組件掛載完成后執(zhí)行殴瘦,清理函數(shù)在組件卸載后執(zhí)行
  2. 依賴發(fā)生變化時執(zhí)行,清理函數(shù)會在依賴發(fā)生變化号杠,useEffect 內(nèi)的邏輯執(zhí)行前調(diào)用

2. 依賴項

  • 依賴項為空蚪腋,則只會在組件掛載完成后執(zhí)行一次。當(dāng)組件再次更新時候姨蟋,不會執(zhí)行屉凯。

  • 如果 React 的所有依賴項都具有與上次渲染期間相同的值,則 React 將跳過 Effect

  • 您不能“選擇”您的依賴項眼溶。它們由 Effect 中的代碼決定悠砚。

  • 依賴項需要是能夠觸發(fā)組件更新的變量,比如 state 或者 props

不需要 effect 的情況

effect 是 React 的應(yīng)急方案堂飞。它允許我們能夠與一些外部系統(tǒng)同步灌旧,比如 ajax 請求和瀏覽器 DOM。如果不涉及外部系統(tǒng)绰筛,則不需要 effect枢泰。刪除不必要的 effect 可以使代碼更容易理解,運行速度更快并且不容易出錯铝噩。

下面是幾種常見的不需要 effect 的情況:

  1. 如果您可以在渲染期間計算某些東西衡蚂,則不需要 Effect。
  2. 要緩存昂貴的計算,請?zhí)砑?useMemo 而不是 useEffect.
  3. 要重置整個組件樹的狀態(tài)讳窟,請將不同的傳遞 key 給它让歼。
  4. 要重置特定位的狀態(tài)以響應(yīng)屬性更改,請在渲染期間設(shè)置它丽啡。
  5. 因為顯示組件而運行的代碼應(yīng)該在 Effects 中谋右,其余的應(yīng)該在事件中。
  6. 如果您需要更新多個組件的狀態(tài)补箍,最好在單個事件期間執(zhí)行改执。
  7. 每當(dāng)您嘗試同步不同組件中的狀態(tài)變量時,請考慮提升狀態(tài)坑雅。
  8. 您可以使用 Effects 獲取數(shù)據(jù)辈挂,但您需要實施清理以避免競爭條件。

具體例子可以參考官網(wǎng)https://react.docschina.org/learn/you-might-not-need-an-effect#caching-expensive-calculations

我的理解就是裹粤,盡可能少用 useEffect终蒂,除非不用不行的情況。

useLayoutEffect

useLayoutEffect 跟 useEffect 唯一的不同就是二者的執(zhí)行時機不同遥诉。

前面說過拇泣,對于一次更新有三個階段:觸發(fā)渲染(render)提交(commit)矮锈。

render 階段主要是組件函數(shù)執(zhí)行霉翔,jsx 轉(zhuǎn)化為 Fiber 等工作。

commit 階段主要是把更改反映到瀏覽器上苞笨,類似 document.appendChild()之類的操作债朵。

useEffect 在 commit 階段完成后執(zhí)行。

useLayoutEffect 在 commit 階段之前執(zhí)行瀑凝。

由于 commit 階段主要是頁面更新的操作男娄,因此useLayoutEffect 會阻塞頁面更新恨樟。

比如這個例子:

import { useState, useEffect, useLayoutEffect } from 'react';

export default function App() {
  const [text, setText] = useState('11111');

  useEffect(() => {
    console.log('useEffect');
    let i = 0;
    while (i < 100000000) {
      i++;
    }
    setText('00000');
  }, []);

  // useLayoutEffect(() => {
  //   console.log("useLayoutEffect");
  //   let i = 0;
  //   while (i < 100000000) {
  //     i++;
  //   }
  //   setText("00000");
  // }, []);

  return <h1>{text}</h1>;
}

使用 useEffect 頁面會有明顯的從 11111 變成 00000 的過程奉狈。使用 useLayoutEffect 則不會央渣。

讓我們梳理下執(zhí)行流程:

useEffect: render => commit(反映到頁面上) => useEffect => render => commit(反映到頁面上)

useLayoutEffect: render => useLayoutEffect => render => commit(反映到頁面上)

useLayoutEffect 執(zhí)行后發(fā)現(xiàn) state 更新,就不再把 11111 反映到頁面上了射窒,直接再次執(zhí)行 react 渲染。因此我們沒有看到從 11111 閃爍成 00000 的過程将塑。

自定義 hook

自定義 hook 是一個函數(shù)脉顿,它允許我們在組件之間共享邏輯。

在使用自定義 hook 之前点寥,我們代碼復(fù)用的最小單元是組件艾疟。使用自定義 hook 之后,我們可以方便地復(fù)用組件里的邏輯。

基本使用

編寫自定義 hook 需要遵循以下規(guī)則:

  1. 命名必須是 use 后跟大寫字母蔽莱,比如useLogin, useForceUpdate

  2. 自定義 hook 中至少要使用一個其他 hook

比如我們寫一個強制刷新的 hook:

import { useState } from 'react';
function useForceUpdate() {
  const [, setForceState] = useState({});
  return () => setForceState({});
}

在組件里使用:

export default function App() {
  const forceUpdate = useForceUpdate();
  console.log('render');
  return <button onClick={forceUpdate}>強制刷新</button>;
}

當(dāng)我們點擊 強制刷新 按鈕的時候弟疆,會打印 render。也就是App組件重新渲染了盗冷。

何時使用

就個人理解怠苔,我覺得有兩種情況比較適合使用自定義 hook:

  1. 有經(jīng)常復(fù)用的組件邏輯時

  2. 使用自定義 hook 后能夠讓代碼邏輯,數(shù)據(jù)流向更清晰

memo

在 React 中仪糖,父組件的重新渲染會導(dǎo)致子組件的重新渲染柑司。memo 允許我們在 props 不變的情況下避免渲染子組件。

語法

memo(Component, arePropsEqual?):包裝一個組件锅劝,并獲得改組件的緩存版本攒驰。

Component: 要包裝的組件。
arePropsEqual(prevProps, nextProps): 接收兩個參數(shù)故爵,前一次的 props 和后一次的 props玻粪。返回值是一個布爾類型,true表示新舊 props 相等诬垂,false表示兩次 props 不相等奶段。

下面用一個例子感受它的用法。

緩存子組件的例子

import React, { useState, memo } from 'react';

function Hello({ text }) {
  console.log('子組件重新渲染');
  return <h1>{`hello ${text}!`}</h1>;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const onAddCount = () => {
    setCount((count) => count + 1);
  };

  const onChangeText = () => {
    setText('world');
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={onAddCount}>+1</button>

      <Hello text={text} />
      <button onClick={onChangeText}>改變子組件文本</button>
    </>
  );
}

當(dāng)我們點擊 +1按鈕時剥纷,會打印 子組件重新渲染痹籍。也就是說當(dāng)我們的父組件更新的時候,子組件也會相應(yīng)更新晦鞋。

但是如果我們用 memo 來包裹子組件蹲缠,代碼如下:

import React, { useState, memo } from 'react';

const Hello = memo(function Hello() {
  console.log('子組件重新渲染');
  return <h1>Hello, world!</h1>;
});

// ...

當(dāng)我們點擊 +1按鈕時, 子組件重新渲染 不會再打印悠垛。也就說我們通過 memo 實現(xiàn)了子組件的緩存线定。

需要注意的是,當(dāng)上下文或者子組件內(nèi)部狀態(tài)變化的時确买,依然會觸發(fā)更新斤讥。 memo 緩存組件只是針對 props 不發(fā)生改變的情況。

prop 是對象湾趾、數(shù)組或函數(shù)的情況

當(dāng)傳遞給子組件的 prop 是對象芭商、數(shù)組或函數(shù)時,由于它們是引用類型搀缠,父組件重新渲染會導(dǎo)致它們被重新定義铛楣。也就是說,props 發(fā)生了變化艺普。這種情況下簸州,依然會觸發(fā)子組件更新鉴竭。

比如下面這個例子

import React, { useState, memo } from 'react';

const List = memo(function List({ list }) {
  console.log('子組件重新渲染');
  return (
    <>
      {list.map((item) => (
        <div key={item.id}>{item.content}</div>
      ))}
    </>
  );
});

export default function App() {
  const [title, setTitle] = useState('父組件');
  const [todoList, setTodoList] = useState([
    { id: 1, content: '吃飯', isDone: true },
    { id: 2, content: '睡覺', isDone: false },
    { id: 3, content: '洗澡', isDone: true },
    { id: 4, content: '刷牙', isDone: false },
    { id: 5, content: '刷抖音', isDone: false }
  ]);

  const changeTitle = () => {
    setTitle('父組件' + Math.random().toFixed(2));
  };

  const list = todoList.filter((item) => item.isDone);

  return (
    <>
      <h1 onClick={changeTitle}>{title}</h1>
      <List list={list} />
    </>
  );
}

點擊父組件,依然會觸發(fā)子組件渲染岸浑。這是由于每次父組件渲染都會重新定義一個變量 list, 兩次的 list 不是同一個引用搏存。

這種情況要怎么處理才能避免子組件渲染呢?有兩種辦法:

1. 使用比較函數(shù)

我們可以給 memo 添加第二個參數(shù)arePropsEqual:

// ...
(prevProps, nextProps) => {
  return (
    prevProps.list.length === nextProps.list.length &&
    prevProps.list.every((item) => {
      let allOk = true;
      for (let key in item) {
        if (prevProps[key] !== nextProps[key]) {
          allOk = false;
        }
      }
      return allOk;
    })
  );
};
//   ...

這樣矢洲,當(dāng)修改 title 時璧眠,list 的內(nèi)容沒有變化,并不會觸發(fā)子組件更新兵钮。

個人建議蛆橡,盡可能避免使用比較函數(shù)。主要出于兩個考慮:一來別人需要閱讀你的比較函數(shù)來確定你的組件更新規(guī)則掘譬;二來我們重寫比較函數(shù)就意味著每次父組件更新都會執(zhí)行比較函數(shù)泰演。如果比較函數(shù)比較復(fù)雜且耗時,那么使用比較函數(shù)就不再是好的選擇了葱轩。

2. 使用 useCallback 或者 useMemo 來緩存引用類型

useCallback 用來緩存一個函數(shù)睦焕。在這個例子里,使用 useMemo 比較合適靴拱。

修改 list 的定義垃喊,代碼如下:

// ...
// 使用useMemo緩存list, 這樣title改變不會再觸發(fā)子組件渲染
const list = useMemo(() => todoList.filter((item) => item.isDone), [todoList]);
// ...

這樣,由于我們緩存了 list, 當(dāng)修改 title 時袜炕,list 仍為同一個 list本谜,并不會觸發(fā)子組件更新。

useMemo

useMemo 允許我們緩存一個計算結(jié)果偎窘。當(dāng)再次渲染的時候乌助,返回上一次的結(jié)果而不是重新計算。

語法

const cachedValue = useMemo(calculateValue, dependencies)

  • calculateValue: 緩存的計算結(jié)果陌知。 當(dāng)它是一個函數(shù)時他托,會緩存這個函數(shù)的返回值。

  • dependencies: 依賴項仆葡。當(dāng)依賴項變化時赏参,重新計算結(jié)果。

使用場景

  1. 防止組件重新渲染

比如前面的例子:

當(dāng) prop 是對象沿盅、數(shù)組或函數(shù)的情況把篓,這時候可以使用 useMemo 配合 memo 緩存組件。

  1. 避免昂貴的計算

比如下面這個例子

import React, { useState, useMemo } from 'react';

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

  // 模擬復(fù)雜的運算嗡呼,需要兩秒鐘
  const getResult = async () => {
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 2000);
    });
    return 2;
  };

  const onAddCount = async () => {
    const result = await getResult();
    setCount((count) => count + result);
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={onAddCount}>+隨機數(shù)</button>
    </>
  );
}

getResult 是一個耗時的計算纸俭,需要兩秒鐘。這就會導(dǎo)致我們每次點擊按鈕南窗,都要等待兩秒才能響應(yīng)。如果我們使用 useMemo 緩存結(jié)果,那么只有第一次需要等待兩秒万伤,后面都會快速響應(yīng)窒悔。

import React, { useState, useMemo } from 'react';

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

  // 使用useMemo緩存復(fù)雜的計算結(jié)果
  const getResult = useMemo(async () => {
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 2000);
    });
    return 2;
  }, []);

  const onAddCount = async () => {
    // 使用useMemo直接緩存計算結(jié)果,getResult是結(jié)果不是函數(shù)
    const result = await getResult;
    setCount((count) => count + result);
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={onAddCount}>+隨機數(shù)</button>
    </>
  );
}

使用 useMemo 前的效果:

使用useMemo前

使用 useMemo 后的效果:

使用useMemo后

useCallback

useMemo 允許我們緩存一個函數(shù)敌买。當(dāng)再次渲染的時候简珠,返回上一次的函數(shù)而不是重新定義。

語法

const cachedFn = useCallback(fn, dependencies)

  • fn: 緩存的函數(shù)虹钮。

  • dependencies: 依賴項聋庵。當(dāng)依賴項變化時,重新計算結(jié)果芙粱。

使用場景

  1. 防止組件重新渲染

當(dāng)我們傳給子組件的屬性有函數(shù)的時候,比如下面這個例子

import React, { useState, memo, useMemo, useCallback } from 'react';

const Hello = memo(function Hello({ text, onClick }) {
  console.log('子組件重新渲染');
  return <h1 onClick={onClick}>{`hello ${text}!`}</h1>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const onAddCount = () => {
    setCount((count) => count + 1);
  };

  const onChangeText = () => {
    setText('world');
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={onAddCount}>+1</button>

      <Hello text={text} onClick={onChangeText} />
    </>
  );
}

當(dāng)我們點擊 count 會造成子組件的渲染祭玉,這是因為 onChangeText 是引用類型,每次父組件渲染春畔,它都被重新定義脱货。這導(dǎo)致了每次 props 都發(fā)生了變化。我們可以使用 useCallback 來緩存 onChangeText:

// 使用useCallback來緩存onChangeText
const onChangeText = useCallback(() => {
  setText('world');
}, []);

使用 useMemo 也可以實現(xiàn)相同的結(jié)果律姨,只不過需要再多包一層函數(shù):

// 使用useMemo緩存onChangeText
const onChangeText = useMemo(() => {
  return () => {
    setText('world');
  };
}, []);
  1. 優(yōu)化自定義 hook
    如果您正在編寫自定義 Hook振峻,建議將它返回的任何函數(shù)包裝到 useCallback:
function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback(
    (url) => {
      dispatch({ type: 'navigate', url });
    },
    [dispatch]
  );

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack
  };
}

這確保了 Hook 的使用者可以在需要時優(yōu)化他們自己的代碼。

爭議

有人認為應(yīng)當(dāng)給所有的函數(shù)包上 useCallback, 我并不認同择份。主要是出于以下兩個考慮:

  1. 使用 useCallback 后代碼可讀性變差
  2. 創(chuàng)建一個函數(shù)的性能消耗幾乎可以忽略不計扣孟,不應(yīng)作為優(yōu)化點

最后

官方文檔內(nèi)容較多,這里只整理個人認為比較常用的知識點荣赶。想要查漏補缺的小伙伴可以去看官網(wǎng)

參考文檔

React官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凤价,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讯壶,更是在濱河造成了極大的恐慌料仗,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伏蚊,死亡現(xiàn)場離奇詭異立轧,居然都是意外死亡,警方通過查閱死者的電腦和手機躏吊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門氛改,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人比伏,你說我怎么就攤上這事胜卤。” “怎么了赁项?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵葛躏,是天一觀的道長澈段。 經(jīng)常有香客問我,道長舰攒,這世上最難降的妖魔是什么败富? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮摩窃,結(jié)果婚禮上兽叮,老公的妹妹穿的比我還像新娘。我一直安慰自己猾愿,他們只是感情好鹦聪,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蒂秘,像睡著了一般泽本。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上材彪,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天观挎,我揣著相機與錄音,去河邊找鬼段化。 笑死嘁捷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的显熏。 我是一名探鬼主播雄嚣,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼喘蟆!你這毒婦竟也來了缓升?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤蕴轨,失蹤者是張志新(化名)和其女友劉穎港谊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橙弱,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡歧寺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棘脐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斜筐。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蛀缝,靈堂內(nèi)的尸體忽然破棺而出顷链,到底是詐尸還是另有隱情,我是刑警寧澤屈梁,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布嗤练,位于F島的核電站榛了,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏潭苞。R本人自食惡果不足惜忽冻,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一真朗、第九天 我趴在偏房一處隱蔽的房頂上張望此疹。 院中可真熱鬧,春花似錦遮婶、人聲如沸蝗碎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹦骑。三九已至,卻和暖如春臀防,著一層夾襖步出監(jiān)牢的瞬間眠菇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工袱衷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捎废,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓致燥,卻偏偏與公主長得像登疗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嫌蚤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容