首頁>技術>

前言目的

本文只介紹函式式元件特有的效能優化方式,類元件和函式式元件都有的不介紹,比如 key 的使用。另外本文不詳細的介紹 API 的使用,後面也許會寫,其實想用好 hooks 還是蠻難的。

面向讀者

有過 React 函式式元件的實踐,並且對 hooks 有過實踐,對 useState、useCallback、useMemo API 至少看過文件,如果你有過對類元件的效能優化經歷,那麼這篇文章會讓你有種熟悉的感覺。

React 效能優化思路

我覺得 React 效能優化的理念的主要方向就是這兩個:

減少重新 render 的次數。因為在 React 裡最重(花時間最長)的一塊就是 reconction(簡單的可以理解為 diff),如果不 render,就不會 reconction。減少計算的量。主要是減少重複計算,對於函式式元件來說,每次 render 都會重新從頭開始執行函式呼叫。

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

但是在函式式元件裡面沒有宣告週期也沒有類,那如何來做效能優化呢?

React.memo

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

可能產生效能問題的例子

舉個 ,首先我們看兩段程式碼:

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

// index.jsimport 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.jsimport React from "react";function Child(props) {  console.log(props.name)  return <h1>{props.name}</h1>}export default Child

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

image-20191030221223045

並且控制檯會列印"桃桃”,證明 Child 元件渲染了。

image-20191030222021717

title 已經改變了,而且控制檯也打印出"桃桃",可以看到雖然我們改的是父元件的狀態,父元件重新渲染了,並且子元件也重新渲染了。你可能會想,傳遞給 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 官網[1]。

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

useCallback

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

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

首先看程式碼:

父元件 index.js

// index.jsimport 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 之後,副標題會變為「副標題改變了」,並且控制檯會再次打印出桃桃,這就證明了子元件又重新渲染了,但是子元件沒有任何變化,那麼這次 Child 元件的重新渲染就是多餘的,那麼如何避免掉這個多餘的渲染呢?

找原因

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

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

要麼是元件自己的狀態改變要麼是父元件重新渲染,導致子元件重新渲染,但是父元件的 props 沒有改版要麼是父元件重新渲染,導致子元件重新渲染,但是父元件傳遞的 props 改變

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

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

那麼就是第三種情況了,當父元件重新渲染的時候,傳遞給子元件的 props 發生了改變,再看傳遞給 Child 元件的就兩個屬性,一個是 name,一個是 onClick ,name 是傳遞的常量,不會變,變的就是 onClick 了,為什麼傳遞給 onClick 的 callback 函式會發生改變呢?在文章的開頭就已經說過了,在函式式元件裡每次重新渲染,函式元件都會重頭開始重新執行,那麼這兩次建立的 callback 函式肯定發生了改變,所以導致了子元件重新渲染。

如何解決

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

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

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

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

// index.jsimport 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 的效能優化方向主要是兩個:一個是減少重新 render 的次數(或者說減少不必要的渲染),另一個是減少計算的量。

前面介紹的 React.memo 和 useCallback 都是為了減少重新 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。

可能產生效能問題

就算是一個看起來很簡單的元件,也有可能產生效能問題,通過這個最簡單的例子來看看還有什麼值得優化的地方。

首先我們把 expensiveFn 函式當做一個計算量很大的函式(比如你可以把 i 換成 10000000),然後當我們每次點選 +1 按鈕的時候,都會重新渲染元件,而且都會呼叫 expensiveFn 函式並輸出 49995000。由於每次呼叫 expensiveFn 所返回的值都一樣,所以我們可以想辦法將計算出來的值快取起來,每次呼叫函式直接返回快取的值,這樣就可以做一些效能優化。

useMemo 做計算結果快取

針對上面產生的問題,就可以用 useMemo 來快取 expensiveFn 函式執行後的值。

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

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

useMemo 的第一個引數就是一個函式,這個函式返回的值會被快取起來,同時這個值會作為 useMemo 的返回值,第二個引數是一個數組依賴,如果數組裡面的值有變化,那麼就會重新去執行第一個引數裡面的函式,並將函式返回的值快取起來並作為 useMemo 的返回值 。

了解了 useMemo 的使用方法,然後就可以對上面的例子進行優化,優化程式碼如下:

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> );}

執行上面的程式碼,然後現在可以觀察無論我們點選 +1多少次,只會輸出一次 49995000,這就代表 expensiveFn 只執行了一次,達到了我們想要的效果。

小結

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

不過另外提醒兩點

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

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

總結

對於效能瓶頸可能對於小專案遇到的比較少,畢竟計算量小、業務邏輯也不復雜,但是對於大專案,很可能是會遇到效能瓶頸的,但是對於效能優化有很多方面:網路、關鍵路徑渲染、打包、圖片、快取等等方面,具體應該去優化哪方面還得自己去排查,本文只介紹了效能優化中的冰山一角:執行過程中 React 的優化。

React 的優化方向:減少 render 的次數;減少重複計算。如何去找到 React 種導致效能問題的方法,見 useCallback 部分。合理的拆分元件其實也是可以做效能優化的,你這麼想,如果你整個頁面只有一個大的元件,那麼當 props 或者 state 變更之後,需要 reconction 的是整個元件,其實你只是變了一個文字,如果你進行了合理的元件拆分,你就可以控制更小粒度的更新。

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

71
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • PHP開發環境安裝配置:Win10+Docker+Laradock(上篇)