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

  • <label id="l1w1f"></label>
    <label id="l1w1f"></label>
    學無先后,達者為師

    網站首頁 編程語言 正文

    React 函數式組件怎樣進行優(yōu)化

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

    前言

    目的

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

    面向讀者

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

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

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

    1. 減少重新 render 的次數。因為在 React 里最重(花時間最長)的一塊就是 reconciliation(簡單的可以理解為 diff),如果不 render,就不會 reconciliation。

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

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

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

    React實戰(zhàn)視頻講解:進入學習

    React.memo

    首先要介紹的就是 React.memo,這個 API 可以說是對標類組件里面的 PureComponent,這是可以減少重新 render 的次數的。

    可能產生性能問題的例子

    舉個??,首先我們看兩段代碼:

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

    // index.js
    import React, { useState } from "react";
    import ReactDOM from "react-dom";
    import Child from './child'
    
    function App() {
      const [title, setTitle] = useState("這是一個 title")
    
      return (
        <div className="App">
          <h1>{ title }</h1>
          <button onClick={() => setTitle("title 已經改變")}>改名字</button>
          <Child name="桃桃"></Child>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    
    

    在同級目錄有一個 child.js

    // child.js
    import React from "react";
    
    function Child(props) {
      console.log(props.name)
      return <h1>{props.name}</h1>
    }
    
    export default Child
    
    

    當首次渲染的時候的效果如下:

    image-20191030221223045

    并且控制臺會打印"桃桃”,證明 Child 組件渲染了。

    接下來點擊改名字這個 button,頁面會變成:

    image-20191030222021717

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

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

    那么我們怎么才能做到在 props 沒有變化的時候,子組件不渲染呢?

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

    React.memo 的基礎用法

    把聲明的組件通過React.memo包一層就好了,React.memo其實是一個高階函數,傳遞一個組件進去,返回一個可以記憶的組件。

    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 不變的情況下,這個被包裹的組件是不會重新渲染的,也就是說上面那個例子,在我點擊改名字之后,僅僅是 title 會變,但是 Child 組件不會重新渲染(表現出來的效果就是 Child 里面的 log 不會在控制臺打印出來),會直接復用最近一次渲染的結果。

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

    React.memo 高級用法

    默認情況下其只會對 props 的復雜對象做淺層對比(淺層對比就是只會對比前后兩次 props 對象引用是否相同,不會對比對象里面的內容是否相同),如果你想要控制對比過程,那么請將自定義的比較函數通過第二個參數傳入來實現。

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

    此部分來自于 React 官網。

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

    useCallback

    現在根據上面的例子,再改一下需求,在上面的需求上增加一個副標題,并且有一個修改副標題的 button,然后把修改標題的 button 放到 Child 組件里。

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

    首先看代碼:

    父組件 index.js

    // index.js
    import React, { useState } from "react";
    import ReactDOM from "react-dom";
    import Child from "./child";
    
    function App() {
      const [title, setTitle] = useState("這是一個 title");
      const [subtitle, setSubtitle] = useState("我是一個副標題");
    
      const callback = () => {
        setTitle("標題改變了");
      };
      return (
        <div className="App">
          <h1>{title}</h1>
          <h2>{subtitle}</h2>
          <button onClick={() => setSubtitle("副標題改變了")}>改副標題</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}>改標題</button>
          <h1>{props.name}</h1>
        </>
      );
    }
    
    export default React.memo(Child);
    
    

    首次渲染的效果

    image-20191031235605228

    這段代碼在首次渲染的時候會顯示上圖的樣子,并且控制臺會打印出桃桃

    然后當我點擊改副標題這個 button 之后,副標題會變?yōu)椤父睒祟}改變了」,并且控制臺會再次打印出桃桃,這就證明了子組件又重新渲染了,但是子組件沒有任何變化,那么這次 Child 組件的重新渲染就是多余的,那么如何避免掉這個多余的渲染呢?

    找原因

    我們在解決問題的之前,首先要知道這個問題是什么原因導致的?

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

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

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

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

    接下來用排除法查出是什么原因導致的:

    第一種很明顯就排除了,當點擊改副標題 的時候并沒有去改變 Child 組件的狀態(tài);

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

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

    如何解決

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

    useCallback 使用方法

    const callback = () => {
      doSomething(a, b);
    }
    
    const memoizedCallback = useCallback(callback, [a, b])
    
    

    把函數以及依賴項作為參數傳入 useCallback,它將返回該回調函數的 memoized 版本,這個 memoizedCallback 只有在依賴項有變化的時候才會更新。

    那么可以將 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("這是一個 title");
      const [subtitle, setSubtitle] = useState("我是一個副標題");
    
      const callback = () => {
        setTitle("標題改變了");
      };
    
      // 通過 useCallback 進行記憶 callback,并將記憶的 callback 傳遞給 Child
      const memoizedCallback = useCallback(callback, [])
    
      return (
        <div className="App">
          <h1>{title}</h1>
          <h2>{subtitle}</h2>
          <button onClick={() => setSubtitle("副標題改變了")}>改副標題</button>
          <Child onClick={memoizedCallback} name="桃桃" />
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    
    

    這樣我們就可以看到只會在首次渲染的時候打印出桃桃,當點擊改副標題和改標題的時候是不會打印桃桃的。

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

    useMemo

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

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

    function App() {
      const [num, setNum] = useState(0);
    
      // 一個非常耗時的一個計算函數
      // 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

    這個例子功能很簡單,就是點擊 +1 按鈕,然后會將現在的值(num) 與 計算函數 (expensiveFn) 調用后的值相加,然后將和設置給 num 并顯示出來,在控制臺會輸出 49995000

    可能產生性能問題

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

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

    useMemo 做計算結果緩存

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

    首先介紹一下 useMemo 的基本的使用方法,詳細的使用方法可見官網:

    function computeExpensiveValue() {
      // 計算量很大的代碼
      return xxx
    }
    
    const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
    
    

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

    了解了 useMemo 的使用方法,然后就可以對上面的例子進行優(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í)行上面的代碼,然后現在可以觀察無論我們點擊 +1多少次,只會輸出一次 49995000,這就代表 expensiveFn 只執(zhí)行了一次,達到了我們想要的效果。

    小結

    useMemo 的使用場景主要是用來緩存計算量比較大的函數結果,可以避免不必要的重復計算,有過 vue 的使用經歷同學可能會覺得跟 Vue 里面的計算屬性有異曲同工的作用。

    不過另外提醒兩點

    一、如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值;

    二、計算量如果很小的計算函數,也可以選擇不使用 useMemo,因為這點優(yōu)化并不會作為性能瓶頸的要點,反而可能使用錯誤還會引起一些性能問題。

    總結

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

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

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

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

    欄目分類
    最近更新