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

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

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

React 函數(shù)式組件怎樣進(jìn)行優(yōu)化

作者:xiaofeng123aazz 更新時(shí)間: 2022-09-26 編程語言

前言

目的

本文只介紹函數(shù)式組件特有的性能優(yōu)化方式,類組件和函數(shù)式組件都有的不介紹,比如 key 的使用。另外本文不詳細(xì)的介紹 API 的使用,后面也許會(huì)寫,其實(shí)想用好 hooks 還是蠻難的。

面向讀者

有過 React 函數(shù)式組件的實(shí)踐,并且對(duì) hooks 有過實(shí)踐,對(duì) useState、useCallback、useMemo API 至少看過文檔,如果你有過對(duì)類組件的性能優(yōu)化經(jīng)歷,那么這篇文章會(huì)讓你有種熟悉的感覺。

React 性能優(yōu)化思路

我覺得React 性能優(yōu)化的理念的主要方向就是這兩個(gè):

  1. 減少重新 render 的次數(shù)。因?yàn)樵?React 里最重(花時(shí)間最長(zhǎng))的一塊就是 reconciliation(簡(jiǎn)單的可以理解為 diff),如果不 render,就不會(huì) reconciliation。

  2. 減少計(jì)算的量。主要是減少重復(fù)計(jì)算,對(duì)于函數(shù)式組件來說,每次 render 都會(huì)重新從頭開始執(zhí)行函數(shù)調(diào)用。

在使用類組件的時(shí)候,使用的 React 優(yōu)化 API 主要是:shouldComponentUpdatePureComponent,這兩個(gè) API 所提供的解決思路都是為了減少重新 render 的次數(shù),主要是減少父組件更新而子組件也更新的情況,雖然也可以在 state 更新的時(shí)候阻止當(dāng)前組件渲染,如果要這么做的話,證明你這個(gè)屬性不適合作為 state,而應(yīng)該作為靜態(tài)屬性或者放在 class 外面作為一個(gè)簡(jiǎn)單的變量 。

但是在函數(shù)式組件里面沒有聲明周期也沒有類,那如何來做性能優(yōu)化呢?

React實(shí)戰(zhàn)視頻講解:進(jìn)入學(xué)習(xí)

React.memo

首先要介紹的就是 React.memo,這個(gè) API 可以說是對(duì)標(biāo)類組件里面的 PureComponent,這是可以減少重新 render 的次數(shù)的。

可能產(chǎn)生性能問題的例子

舉個(gè)?,首先我們看兩段代碼:

在根目錄有一個(gè) index.js,代碼如下,實(shí)現(xiàn)的東西大概就是:上面一個(gè) title,中間一個(gè) button(點(diǎn)擊 button 修改 title),下面一個(gè)木偶組件,傳遞一個(gè) name 進(jìn)去。

// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from './child'

function App() {
  const [title, setTitle] = useState("這是一個(gè) title")

  return (
    <div className="App">
      <h1>{ title }</h1>
      <button onClick={() => setTitle("title 已經(jīng)改變")}>改名字</button>
      <Child name="桃桃"></Child>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


在同級(jí)目錄有一個(gè) child.js

// child.js
import React from "react";

function Child(props) {
  console.log(props.name)
  return <h1>{props.name}</h1>
}

export default Child

當(dāng)首次渲染的時(shí)候的效果如下:

image-20191030221223045

并且控制臺(tái)會(huì)打印"桃桃”,證明 Child 組件渲染了。

接下來點(diǎn)擊改名字這個(gè) button,頁面會(huì)變成:

image-20191030222021717

title 已經(jīng)改變了,而且控制臺(tái)也打印出"桃桃",可以看到雖然我們改的是父組件的狀態(tài),父組件重新渲染了,并且子組件也重新渲染了。你可能會(huì)想,傳遞給 Child 組件的 props 沒有變,要是 Child 組件不重新渲染就好了,為什么會(huì)這么想呢?

我們假設(shè) Child 組件是一個(gè)非常大的組件,渲染一次會(huì)消耗很多的性能,那么我們就應(yīng)該盡量減少這個(gè)組件的渲染,否則就容易產(chǎn)生性能問題,所以子組件如果在 props 沒有變化的情況下,就算父組件重新渲染了,子組件也不應(yīng)該渲染。

那么我們?cè)趺床拍茏龅皆?props 沒有變化的時(shí)候,子組件不渲染呢?

答案就是用 React.memo 在給定相同 props 的情況下渲染相同的結(jié)果,并且通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)。

React.memo 的基礎(chǔ)用法

把聲明的組件通過React.memo包一層就好了,React.memo其實(shí)是一個(gè)高階函數(shù),傳遞一個(gè)組件進(jìn)去,返回一個(gè)可以記憶的組件。

function Component(props) {
   /* 使用 props 渲染 */
}
const MyComponent = React.memo(Component);

那么上面例子的 Child 組件就可以改成這樣:

import React from "react";

function Child(props) {
  console.log(props.name)
  return <h1>{props.name}</h1>
}

export default React.memo(Child)

通過 React.memo 包裹的組件在 props 不變的情況下,這個(gè)被包裹的組件是不會(huì)重新渲染的,也就是說上面那個(gè)例子,在我點(diǎn)擊改名字之后,僅僅是 title 會(huì)變,但是 Child 組件不會(huì)重新渲染(表現(xiàn)出來的效果就是 Child 里面的 log 不會(huì)在控制臺(tái)打印出來),會(huì)直接復(fù)用最近一次渲染的結(jié)果。

這個(gè)效果基本跟類組件里面的 PureComponent效果極其類似,只是前者用于函數(shù)組件,后者用于類組件。

React.memo 高級(jí)用法

默認(rèn)情況下其只會(huì)對(duì) props 的復(fù)雜對(duì)象做淺層對(duì)比(淺層對(duì)比就是只會(huì)對(duì)比前后兩次 props 對(duì)象引用是否相同,不會(huì)對(duì)比對(duì)象里面的內(nèi)容是否相同),如果你想要控制對(duì)比過程,那么請(qǐng)將自定義的比較函數(shù)通過第二個(gè)參數(shù)傳入來實(shí)現(xiàn)。

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

此部分來自于 React 官網(wǎng)。

如果你有在類組件里面使用過 shouldComponentUpdate() 這個(gè)方法,你會(huì)對(duì) React.memo 的第二個(gè)參數(shù)非常的熟悉,不過值得注意的是,如果 props 相等,areEqual 會(huì)返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。

useCallback

現(xiàn)在根據(jù)上面的例子,再改一下需求,在上面的需求上增加一個(gè)副標(biāo)題,并且有一個(gè)修改副標(biāo)題的 button,然后把修改標(biāo)題的 button 放到 Child 組件里。

把修改標(biāo)題的 button 放到 Child 組件的目的是,將修改 title 的事件通過 props 傳遞給 Child 組件,然后觀察這個(gè)事件可能會(huì)引起性能問題。

首先看代碼:

父組件 index.js

// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from "./child";

function App() {
  const [title, setTitle] = useState("這是一個(gè) title");
  const [subtitle, setSubtitle] = useState("我是一個(gè)副標(biāo)題");

  const callback = () => {
    setTitle("標(biāo)題改變了");
  };
  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副標(biāo)題改變了")}>改副標(biāo)題</button>
      <Child onClick={callback} name="桃桃" />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


子組件 child.js

import React from "react";

function Child(props) {
  console.log(props);
  return (
    <>
      <button onClick={props.onClick}>改標(biāo)題</button>
      <h1>{props.name}</h1>
    </>
  );
}

export default React.memo(Child);

首次渲染的效果

image-20191031235605228

這段代碼在首次渲染的時(shí)候會(huì)顯示上圖的樣子,并且控制臺(tái)會(huì)打印出桃桃

然后當(dāng)我點(diǎn)擊改副標(biāo)題這個(gè) button 之后,副標(biāo)題會(huì)變?yōu)椤父睒?biāo)題改變了」,并且控制臺(tái)會(huì)再次打印出桃桃,這就證明了子組件又重新渲染了,但是子組件沒有任何變化,那么這次 Child 組件的重新渲染就是多余的,那么如何避免掉這個(gè)多余的渲染呢?

找原因

我們?cè)诮鉀Q問題的之前,首先要知道這個(gè)問題是什么原因?qū)е碌模?/strong>

咱們來分析,一個(gè)組件重新重新渲染,一般三種情況:

  1. 要么是組件自己的狀態(tài)改變

  2. 要么是父組件重新渲染,導(dǎo)致子組件重新渲染,但是父組件的 props 沒有改版

  3. 要么是父組件重新渲染,導(dǎo)致子組件重新渲染,但是父組件傳遞的 props 改變

接下來用排除法查出是什么原因?qū)е碌模?/p>

第一種很明顯就排除了,當(dāng)點(diǎn)擊改副標(biāo)題 的時(shí)候并沒有去改變 Child 組件的狀態(tài);

第二種情況好好想一下,是不是就是在介紹 React.memo 的時(shí)候情況,父組件重新渲染了,父組件傳遞給子組件的 props 沒有改變,但是子組件重新渲染了,我們這個(gè)時(shí)候用 React.memo 來解決了這個(gè)問題,所以這種情況也排除。

那么就是第三種情況了,當(dāng)父組件重新渲染的時(shí)候,傳遞給子組件的 props 發(fā)生了改變,再看傳遞給 Child 組件的就兩個(gè)屬性,一個(gè)是 name,一個(gè)是 onClickname 是傳遞的常量,不會(huì)變,變的就是 onClick 了,為什么傳遞給 onClick 的 callback 函數(shù)會(huì)發(fā)生改變呢?在文章的開頭就已經(jīng)說過了,在函數(shù)式組件里每次重新渲染,函數(shù)組件都會(huì)重頭開始重新執(zhí)行,那么這兩次創(chuàng)建的 callback 函數(shù)肯定發(fā)生了改變,所以導(dǎo)致了子組件重新渲染。

如何解決

找到問題的原因了,那么解決辦法就是在函數(shù)沒有改變的時(shí)候,重新渲染的時(shí)候保持兩個(gè)函數(shù)的引用一致,這個(gè)時(shí)候就要用到 useCallback 這個(gè) API 了。

useCallback 使用方法

const callback = () => {
  doSomething(a, b);
}

const memoizedCallback = useCallback(callback, [a, b])

把函數(shù)以及依賴項(xiàng)作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,這個(gè) memoizedCallback 只有在依賴項(xiàng)有變化的時(shí)候才會(huì)更新。

那么可以將 index.js 修改為這樣:

// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";

function App() {
  const [title, setTitle] = useState("這是一個(gè) title");
  const [subtitle, setSubtitle] = useState("我是一個(gè)副標(biāo)題");

  const callback = () => {
    setTitle("標(biāo)題改變了");
  };

  // 通過 useCallback 進(jìn)行記憶 callback,并將記憶的 callback 傳遞給 Child
  const memoizedCallback = useCallback(callback, [])

  return (
    <div className="App">
      <h1>{title}</h1>
      <h2>{subtitle}</h2>
      <button onClick={() => setSubtitle("副標(biāo)題改變了")}>改副標(biāo)題</button>
      <Child onClick={memoizedCallback} name="桃桃" />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


這樣我們就可以看到只會(huì)在首次渲染的時(shí)候打印出桃桃,當(dāng)點(diǎn)擊改副標(biāo)題和改標(biāo)題的時(shí)候是不會(huì)打印桃桃的。

如果我們的 callback 傳遞了參數(shù),當(dāng)參數(shù)變化的時(shí)候需要讓它重新添加一個(gè)緩存,可以將參數(shù)放在 useCallback 第二個(gè)參數(shù)的數(shù)組中,作為依賴的形式,使用方式跟 useEffect 類似。

useMemo

在文章的開頭就已經(jīng)介紹了,React 的性能優(yōu)化方向主要是兩個(gè):一個(gè)是減少重新 render 的次數(shù)(或者說減少不必要的渲染),另一個(gè)是減少計(jì)算的量。

前面介紹的 React.memouseCallback 都是為了減少重新 render 的次數(shù)。對(duì)于如何減少計(jì)算的量,就是 useMemo 來做的,接下來我們看例子。

function App() {
  const [num, setNum] = useState(0);

  // 一個(gè)非常耗時(shí)的一個(gè)計(jì)算函數(shù)
  // result 最后返回的值是 49995000
  function expensiveFn() {
    let result = 0;

    for (let i = 0; i < 10000; i++) {
      result += i;
    }

    console.log(result) // 49995000
    return result;
  }

  const base = expensiveFn();

  return (
    <div className="App">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}

首次渲染的效果如下:

useMemo

這個(gè)例子功能很簡(jiǎn)單,就是點(diǎn)擊 +1 按鈕,然后會(huì)將現(xiàn)在的值(num) 與 計(jì)算函數(shù) (expensiveFn) 調(diào)用后的值相加,然后將和設(shè)置給 num 并顯示出來,在控制臺(tái)會(huì)輸出 49995000

可能產(chǎn)生性能問題

就算是一個(gè)看起來很簡(jiǎn)單的組件,也有可能產(chǎn)生性能問題,通過這個(gè)最簡(jiǎn)單的例子來看看還有什么值得優(yōu)化的地方。

首先我們把 expensiveFn 函數(shù)當(dāng)做一個(gè)計(jì)算量很大的函數(shù)(比如你可以把 i 換成 10000000),然后當(dāng)我們每次點(diǎn)擊 +1 按鈕的時(shí)候,都會(huì)重新渲染組件,而且都會(huì)調(diào)用 expensiveFn 函數(shù)并輸出 49995000。由于每次調(diào)用 expensiveFn 所返回的值都一樣,所以我們可以想辦法將計(jì)算出來的值緩存起來,每次調(diào)用函數(shù)直接返回緩存的值,這樣就可以做一些性能優(yōu)化。

useMemo 做計(jì)算結(jié)果緩存

針對(duì)上面產(chǎn)生的問題,就可以用 useMemo 來緩存 expensiveFn 函數(shù)執(zhí)行后的值。

首先介紹一下 useMemo 的基本的使用方法,詳細(xì)的使用方法可見官網(wǎng):

function computeExpensiveValue() {
  // 計(jì)算量很大的代碼
  return xxx
}

const memoizedValue = useMemo(computeExpensiveValue, [a, b]);

useMemo 的第一個(gè)參數(shù)就是一個(gè)函數(shù),這個(gè)函數(shù)返回的值會(huì)被緩存起來,同時(shí)這個(gè)值會(huì)作為 useMemo 的返回值,第二個(gè)參數(shù)是一個(gè)數(shù)組依賴,如果數(shù)組里面的值有變化,那么就會(huì)重新去執(zhí)行第一個(gè)參數(shù)里面的函數(shù),并將函數(shù)返回的值緩存起來并作為 useMemo 的返回值 。

了解了 useMemo 的使用方法,然后就可以對(duì)上面的例子進(jìn)行優(yōu)化,優(yōu)化代碼如下:

function App() {
  const [num, setNum] = useState(0);

  function expensiveFn() {
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    console.log(result)
    return result;
  }

  const base = useMemo(expensiveFn, []);

  return (
    <div className="App">
      <h1>count:{num}</h1>
      <button onClick={() => setNum(num + base)}>+1</button>
    </div>
  );
}

執(zhí)行上面的代碼,然后現(xiàn)在可以觀察無論我們點(diǎn)擊 +1多少次,只會(huì)輸出一次 49995000,這就代表 expensiveFn 只執(zhí)行了一次,達(dá)到了我們想要的效果。

小結(jié)

useMemo 的使用場(chǎng)景主要是用來緩存計(jì)算量比較大的函數(shù)結(jié)果,可以避免不必要的重復(fù)計(jì)算,有過 vue 的使用經(jīng)歷同學(xué)可能會(huì)覺得跟 Vue 里面的計(jì)算屬性有異曲同工的作用。

不過另外提醒兩點(diǎn)

一、如果沒有提供依賴項(xiàng)數(shù)組,useMemo 在每次渲染時(shí)都會(huì)計(jì)算新的值;

二、計(jì)算量如果很小的計(jì)算函數(shù),也可以選擇不使用 useMemo,因?yàn)檫@點(diǎn)優(yōu)化并不會(huì)作為性能瓶頸的要點(diǎn),反而可能使用錯(cuò)誤還會(huì)引起一些性能問題。

總結(jié)

對(duì)于性能瓶頸可能對(duì)于小項(xiàng)目遇到的比較少,畢竟計(jì)算量小、業(yè)務(wù)邏輯也不復(fù)雜,但是對(duì)于大項(xiàng)目,很可能是會(huì)遇到性能瓶頸的,但是對(duì)于性能優(yōu)化有很多方面:網(wǎng)絡(luò)、關(guān)鍵路徑渲染、打包、圖片、緩存等等方面,具體應(yīng)該去優(yōu)化哪方面還得自己去排查,本文只介紹了性能優(yōu)化中的冰山一角:運(yùn)行過程中 React 的優(yōu)化。

  1. React 的優(yōu)化方向:減少 render 的次數(shù);減少重復(fù)計(jì)算。
  2. 如何去找到 React 中導(dǎo)致性能問題的方法,見 useCallback 部分。
  3. 合理的拆分組件其實(shí)也是可以做性能優(yōu)化的,你這么想,如果你整個(gè)頁面只有一個(gè)大的組件,那么當(dāng) props 或者 state 變更之后,需要 reconciliation 的是整個(gè)組件,其實(shí)你只是變了一個(gè)文字,如果你進(jìn)行了合理的組件拆分,你就可以控制更小粒度的更新。

合理拆分組件還有很多其他好處,比如好維護(hù),而且這是學(xué)習(xí)組件化思想的第一步,合理的拆分組件又是一門藝術(shù)了,如果拆分得不合理,就有可能導(dǎo)致狀態(tài)混亂,多敲代碼多思考。

原文鏈接:https://blog.csdn.net/xiaofeng123aazz/article/details/127041958