日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

React.memo?React.useMemo對(duì)項(xiàng)目性能優(yōu)化使用詳解_React

作者:熬夜冠軍 ? 更新時(shí)間: 2023-03-15 編程語(yǔ)言

React.memo

這篇文章會(huì)詳細(xì)介紹該何時(shí)、如何正確使用它,并且搭配 React.memo 來(lái)對(duì)我們的項(xiàng)目進(jìn)行一個(gè)性能優(yōu)化。

示例

我們先從一個(gè)簡(jiǎn)單的示例入手

以下是一個(gè)常規(guī)的父子組件關(guān)系,打開(kāi)瀏覽器控制臺(tái)并觀察,每次點(diǎn)擊父組件中的 + 號(hào)按鈕,都會(huì)導(dǎo)致子組件渲染。

const ReactNoMemoDemo = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <Child name="Son" />
    </div>
  );
};
const Child = props => {
  console.log('子組件渲染了');
  return <p>Child Name: {props.name}</p>;
};
render(<ReactNoMemoDemo />);

子組件的 name 參數(shù)明明沒(méi)有被修改,為什么還是重新渲染?

這就是 React 的渲染機(jī)制,組件內(nèi)部的 state 或者 props 一旦發(fā)生修改,整個(gè)組件樹(shù)都會(huì)被重新渲染一次,即時(shí)子組件的參數(shù)沒(méi)有被修改,甚至無(wú)狀態(tài)組件。

如何處理這個(gè)問(wèn)題?接下里就要說(shuō)到 React.memo

介紹

React.memo 是 React 官方提供的一個(gè)高階組件,用于緩存我們的需要優(yōu)化的組件

如果你的組件在相同 props 的情況下渲染相同的結(jié)果,那么你可以通過(guò)將其包裝在 React.memo 中調(diào)用,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。

讓我們來(lái)改進(jìn)一下上述的代碼,只需要使用 React.memo 組件包裹起來(lái)即可,其他用法不變

使用

function ReactMemoDemo() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <Child name="Son" />
    </div>
  );
}
const Child = React.memo(props => {
  console.log('子組件渲染了');
  return <p>Child Name: {props.name}</p>;
});
render(<ReactMemoDemo />);

再次觀察控制臺(tái),應(yīng)該會(huì)發(fā)現(xiàn)再點(diǎn)擊父組件的按鈕,子組件已經(jīng)不會(huì)重新渲染了。

這就是 React.memo 為我們做的緩存優(yōu)化,渲染 Child 組件之前,對(duì)比 props,發(fā)現(xiàn) name 沒(méi)有發(fā)生改變,因此返回了組件上一次的渲染的結(jié)果。

React.memo 僅檢查 props 變更。如果函數(shù)組件被 React.memo 包裹,且其實(shí)現(xiàn)中擁有 useState,useReducer 或 useContext 的 Hook,當(dāng) state 或 context 發(fā)生變化時(shí),它仍會(huì)重新渲染。

當(dāng)然,如果我們子組件有內(nèi)部狀態(tài)并且發(fā)生了修改,依然會(huì)重新渲染(正常行為)。

FAQ

看到這里,不禁會(huì)產(chǎn)生疑問(wèn),既然如此,那我直接為每個(gè)組件都添加 React.memo 來(lái)進(jìn)行緩存就好了,再深究一下,為什么 React 不直接默認(rèn)為每個(gè)組件緩存呢?那這樣既節(jié)省了開(kāi)發(fā)者的代碼,又為項(xiàng)目帶來(lái)了許多性能的優(yōu)化,這樣不好嗎?

使用太多的緩存,反而容易帶來(lái) 負(fù)提升

前面有說(shuō)到,組件使用緩存策略后,在被更新之前,會(huì)比較最新的 props 和上一次的 props 是否發(fā)生值修改,既然有比較,那就有計(jì)算,如果子組件的參數(shù)特別多且復(fù)雜繁重,那么這個(gè)比較的過(guò)程也會(huì)十分的消耗性能,甚至高于 虛擬 DOM 的生成,這時(shí)的緩存優(yōu)化,反而產(chǎn)生的負(fù)面影響,這個(gè)就是關(guān)鍵問(wèn)題。

當(dāng)然,這種情況很少,大部分情況還是 組件樹(shù)的 虛擬 DOM 計(jì)算比緩存計(jì)算更消耗性能。但是,既然有這種極端問(wèn)題發(fā)生,就應(yīng)該把選擇權(quán)交給開(kāi)發(fā)者,讓我們自行決定是否需要對(duì)該組件進(jìn)行渲染,這也是 React 不默認(rèn)為組件設(shè)置緩存的原因。

也因此,在 React 社區(qū)中,開(kāi)發(fā)者們也一致的認(rèn)為,不必要的情況下,不需要使用 React.memo

什么時(shí)候該用? 組件渲染過(guò)程特別消耗性能,以至于能感覺(jué)到到,比如:長(zhǎng)列表、圖表等

什么時(shí)候不該用?組件參數(shù)結(jié)構(gòu)十分龐大復(fù)雜,比如未知層級(jí)的對(duì)象,或者列表(城市,用戶(hù)名)等

React.memo 二次優(yōu)化

React.memo 默認(rèn)每次會(huì)對(duì)復(fù)雜的對(duì)象做對(duì)比,如果你使用了 React.memo 緩存的組件參數(shù)十分復(fù)雜,且只有參數(shù)屬性?xún)?nèi)的某些/某個(gè)字段會(huì)修改,或者根本不可能發(fā)生變化的情況下,你可以再粒度化的控制對(duì)比邏輯,通過(guò) React.memo 第二個(gè)參數(shù)

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function shouldMemo(prevProps, nextProps) {
  /*
  如果把 nextProps 傳入 render 方法的返回結(jié)果與
  將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true,
  否則返回 false
  */
}
export default React.memo(MyComponent, shouldMemo);

如果對(duì) class 組件有了解過(guò)的朋友應(yīng)該知道,class 組件有一個(gè)生命周期叫做 shouldComponentUpdate(),也是通過(guò)對(duì)比 props 來(lái)告訴組件是否需要更新,但是與這個(gè)邏輯剛好相反。

小結(jié)

對(duì)于 React.memo,無(wú)需刻意去使用它進(jìn)行緩存組件,除非你能感覺(jué)到你需要。另外,不緩存的組件會(huì)多次的觸發(fā) render,因此,如果你在組件內(nèi)有打印信息,可能會(huì)被多次的觸發(fā),也不用去擔(dān)心,即使強(qiáng)制被 rerender,因?yàn)闋顟B(tài)沒(méi)有發(fā)生改變,因此每次 render 返回的值還是一樣,所以也不會(huì)觸發(fā)真實(shí) dom 的更新,對(duì)頁(yè)面實(shí)際沒(méi)有任何影響。

useMemo

示例

同樣,我們先看一個(gè)例子,calculatedCount 變量是一個(gè)假造的比較消耗性能的計(jì)算表達(dá)式,為了方便顯示性能數(shù)據(jù)打印時(shí)間,我們使用了 IIFE 立即執(zhí)行函數(shù),每次計(jì)算 calculatedCount 都會(huì)輸出它的計(jì)算消耗時(shí)間。

打開(kāi)控制臺(tái),因?yàn)槭?IIFE,所以首次會(huì)直接打印出時(shí)間。然后,再點(diǎn)擊 + 號(hào),會(huì)發(fā)現(xiàn)再次打印出了計(jì)算耗時(shí)。這是因?yàn)?React 組件重渲染的時(shí)候,不僅是 jsx,而且變量,函數(shù)這種也全部都會(huì)再次聲明一次,因此導(dǎo)致了 calculatedCount 重新執(zhí)行了初始化(計(jì)算),但是這個(gè)變量值并沒(méi)有發(fā)生改變,如果每次渲染都要重新計(jì)算,那也是十分的消耗性能。

注意觀察,在計(jì)算期間,頁(yè)面會(huì)發(fā)生卡死,不能操作,這是 JS 引擎 的機(jī)制,在執(zhí)行任務(wù)的時(shí)候,頁(yè)面永遠(yuǎn)不會(huì)進(jìn)行渲染,直到任務(wù)結(jié)束為止。這個(gè)過(guò)程對(duì)用戶(hù)體驗(yàn)來(lái)說(shuō)是致命的,雖然我們可以通過(guò)微任務(wù)去處理這個(gè)計(jì)算過(guò)程,從而避免頁(yè)面的渲染阻塞,但是消耗性能這個(gè)問(wèn)題仍然存在,我們需要通過(guò)其他方式去解決。

function UseMemoDemo() {
  const [count, setCount] = React.useState(0);
  const calculatedCount = (() => {
    let res = 0;
    const startTime = Date.now();
    for (let i = 0; i <= 100000000; i++) {
      res++;
    }
    console.log(`Calculated Count 計(jì)算耗時(shí):${Date.now() - startTime} ms`);
    return res;
  })();
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <div>Calculated Count: {calculatedCount}</div>
    </div>
  );
}

介紹

const memoizedValue = useMemo(() => {
  // 處理復(fù)雜計(jì)算,并 return 結(jié)果
}, []);

useMemo 返回一個(gè)緩存過(guò)的值,把 "創(chuàng)建" 函數(shù)和依賴(lài)項(xiàng)數(shù)組作為參數(shù)傳入 useMemo,它僅會(huì)在某個(gè)依賴(lài)項(xiàng)改變時(shí)才重新計(jì)算 memoized 值。這種優(yōu)化有助于避免在每次渲染時(shí)都進(jìn)行高開(kāi)銷(xiāo)的計(jì)算

第一個(gè)參數(shù)是函數(shù),函數(shù)中需要返回計(jì)算值

第二個(gè)參數(shù)是依賴(lài)數(shù)組

  • 如果不傳,則每次都會(huì)初始化,緩存失敗
  • 如果傳空數(shù)組,則永遠(yuǎn)都會(huì)返回第一次執(zhí)行的結(jié)果
  • 如果傳狀態(tài),則在依賴(lài)的狀態(tài)變化時(shí),才會(huì)從新計(jì)算,如果這個(gè)緩存狀態(tài)依賴(lài)了其他狀態(tài)的話,則需要提供進(jìn)去。

這下就很好理解了,我們的 calculatedCount 沒(méi)有任何外部依賴(lài),因此只需要傳遞空數(shù)組作為第二個(gè)參數(shù),開(kāi)始改造

使用

function UseMemoDemo() {
  const [count, setCount] = React.useState(0);
  const calculatedCount = useMemo(() => {
    let res = 0;
    const startTime = Date.now();
    for (let i = 0; i <= 100000000; i++) {
      res++;
    }
    console.log(`Memo Calculated Count 計(jì)算耗時(shí):${Date.now() - startTime} ms`);
    return res;
  }, []);
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <div>Memorized Calculated Count: {calculatedCount}</div>
    </div>
  );
}

現(xiàn)在,"Memo Calculated Count 計(jì)算耗時(shí)"的輸出信息永遠(yuǎn)只會(huì)打印一次,因?yàn)樗粺o(wú)限緩存了。

FAQ何時(shí)使用?

當(dāng)你的表達(dá)式十分復(fù)雜需要經(jīng)過(guò)大量計(jì)算的時(shí)候

示例

下面示例中,我們使用狀態(tài)提升,將子組件的 click 事件函數(shù)放在了父組件中,點(diǎn)擊父組件的 + 號(hào),發(fā)現(xiàn)子組件被重新渲染

const FunctionPropDemo = () => {
  const [count, setCount] = React.useState(0);
  const handleChildClick = () => {
    //
  };
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <Child onClick={handleChildClick} />
    </div>
  );
};
const Child = React.memo(props => {
  console.log('子組件渲染了');
  return (
    <div>
      <div>Child</div>
      <button onClick={props.onClick}>Click Me</button>
    </div>
  );
});
render(<FunctionPropDemo />);

于是我們想到用 memo 函數(shù)包裹子組件,給緩存起來(lái)

const FunctionPropDemo = () => {
  const [count, setCount] = React.useState(0);
  const handleChildClick = () => {
    //
  };
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <Child onClick={handleChildClick} />
    </div>
  );
};
const Child = React.memo(props => {
  console.log('子組件渲染了');
  return (
    <div>
      <div>Child</div>
      <button onClick={props.onClick}>Click Me</button>
    </div>
  );
});
render(<FunctionPropDemo />);

但是意外來(lái)了,即使被 memo 包裹的組件,還是被重新渲染了,為什么!

我們來(lái)逐一分析

  • 首先,點(diǎn)擊父組件的 + 號(hào),count 發(fā)生變化,于是父組件開(kāi)始重渲染
  • 內(nèi)部的未經(jīng)處理的變量和函數(shù)都被重新初始化,useState 不會(huì)再初始化了, useEffect 鉤子函數(shù)重新執(zhí)行,虛擬 dom 更新
  • 執(zhí)行到 Child 組件的時(shí)候,Child 準(zhǔn)備更新,但是因?yàn)樗?memo 緩存組件,于是開(kāi)始淺比較 props 參數(shù),到這里為止一切正常
  • Child 組件參數(shù)開(kāi)始逐一比較變更,到了 onClick 函數(shù),發(fā)現(xiàn)值為函數(shù),提供的新值也為函數(shù),但是因?yàn)閯倓傇诟附M件內(nèi)部重渲染時(shí)被重新初始化了(生成了新的地址),因?yàn)楹瘮?shù)是引用類(lèi)型值,導(dǎo)致引用地址發(fā)生改變!比較結(jié)果為不相等, React 仍會(huì)認(rèn)為它已更改,因此重新發(fā)生了渲染。

既然函數(shù)重新渲染會(huì)被重新初始化生成新的引用地址,因此我們應(yīng)該避免它重新初始化。這個(gè)時(shí)候,useMemo 的第二個(gè)使用場(chǎng)景就來(lái)了

const FunctionPropDemo = () => {
  const [count, setCount] = React.useState(0);
  const handleChildClick = useMemo(() => {
    return () => {
      //
    };
  }, []);
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      <Child onClick={handleChildClick} />
    </div>
  );
};
const Child = React.memo(props => {
  console.log('子組件渲染了');
  return (
    <div>
      <div>Child</div>
      <button onClick={props.onClick}>Click Me</button>
    </div>
  );
});
render(<FunctionPropDemo />);

這里我們將原本的 handleChildClick 函數(shù)通過(guò) useMemo 包裹起來(lái)了,另外函數(shù)永遠(yuǎn)不會(huì)發(fā)生改變,因此傳遞第二參數(shù)為空數(shù)組,再次嘗試點(diǎn)擊 + 號(hào),子組件不會(huì)被重新渲染了。

對(duì)于對(duì)象,數(shù)組,renderProps(參數(shù)為 react 組件) 等參數(shù),都可以使用 useMemo 進(jìn)行緩存

示例

既然 useMemo 可以緩存變量函數(shù)等,那組件其實(shí)也是一個(gè)函數(shù),能不能被緩存呢?我們?cè)囈辉?/p>

繼續(xù)使用第一個(gè)案例,將 React.memo 移除,使用 useMemo 改造

const ReactNoMemoDemo = () => {
  const [count, setCount] = React.useState(0);
  const memorizedChild = useMemo(() => <Child name="Son" />, []);
  return (
    <div>
      <div>Parent Count: {count}</div>
      <button onClick={() => setCount(count => count + 1)}>+</button>
      {memorizedChild}
    </div>
  );
};
const Child = props => {
  console.log('子組件渲染了');
  return <p>Child Name: {props.name}</p>;
};
render(<ReactNoMemoDemo />);

嘗試點(diǎn)擊 + 號(hào),是的,ChilduseMemo 緩存成功了!

小結(jié)

同樣的,不是必要的情況下,和 React.memo 一樣,不需要特別的使用 useMemo

使用場(chǎng)景

  • 表達(dá)式有復(fù)雜計(jì)算且不會(huì)頻發(fā)觸發(fā)更新
  • 引用類(lèi)型的組件參數(shù),函數(shù),對(duì)象,數(shù)組等(一般情況下對(duì)象和數(shù)組都會(huì)從 useState 初始化,useState 不會(huì)二次執(zhí)行,主要是函數(shù)參數(shù))
  • react 組件的緩存

擴(kuò)展

useCallback

前面使用 useMemo 包裹了函數(shù),會(huì)感覺(jué)代碼結(jié)構(gòu)非常的奇怪

const handleChildClick = useMemo(() => {
  return () => {
    //
  };
}, []);

函數(shù)中又 return 了一個(gè)函數(shù),其實(shí)還有另一個(gè)推薦的 APIuseCallback 來(lái)代替于對(duì)函數(shù)的緩存,兩者功能是完全一樣,只是使用方法的區(qū)別,useMemo 需要從第一個(gè)函數(shù)參數(shù)中 return 出要緩存的函數(shù),useCallback 則直接將函數(shù)傳入第一個(gè)參數(shù)即可

const handleChildClick = useCallback(() => {
  //
}, []);

代碼風(fēng)格上簡(jiǎn)介明了了許多

看完這篇文章,相信你對(duì) React.memoReact.useMemo 已經(jīng)有了一定的了解,并且知道何時(shí)/如何使用它們了

原文鏈接:https://juejin.cn/post/7188041140963115066

欄目分類(lèi)
最近更新