[譯]使用React.memo()來優化函式元件的效能

進擊的大蔥發表於2019-03-18

原文連結: Improving Performance in React Functional Component using React.memo
原文作者: Chidume Nnamdi
譯者: 進擊的大蔥
推薦理由: 本文講述了開發React應用時如何使用shouldComponentUpdate生命週期函式以及PureComponent去避免類元件進行無用的重渲染,以及如何使用最新的React.memo API去優化函式元件的效能。

React核心開發團隊一直都努力地讓React變得更快。在React中可以用來優化元件效能的方法大概有以下幾種:

  • 元件懶載入(React.lazy(...)和<Suspense />)
  • Pure Component
  • shouldComponentUpdate(...){...}生命週期函式

本文還會介紹React16.6加入的另外一個專門用來優化函式元件(Functional Component)效能的方法: React.memo

無用的渲染

元件是構成React檢視的一個基本單元。有些元件會有自己本地的狀態(state), 當它們的值由於使用者的操作而發生改變時,元件就會重新渲染。在一個React應用中,一個元件可能會被頻繁地進行渲染。這些渲染雖然有一小部分是必須的,不過大多數都是無用的,它們的存在會大大降低我們應用的效能。

看下面這個例子:

import React from 'react';

class TestC extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
    }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
        
    }
    
    render() {
        return (
            <div >
            {this.state.count}
            <button onClick={()=>this.setState({count: 1})}>Click Me</button>
            </div>
        );
    }
}
export default TestC;
複製程式碼

TestC元件有一個本地狀態count,它的初始值是0(state = {count: 0})。當我們點選Click Me按鈕時,count的值被設定為1。這時候螢幕的數字將會由0變成1。當我們再次點選該按鈕時,count的值還是1, 這時候TestC元件不應該被重新渲染,可是現實是這樣的嗎?

為了測試count重複設定相同的值元件會不會被重新渲染, 我為TestC元件新增了兩個生命週期函式: componentWillUpdate和componentDidUpdate。componentWillUpdate方法在元件將要被重新渲染時被呼叫,而componentDidUpdate方法會在元件成功重渲染後被呼叫。

在瀏覽器中執行我們的程式碼,然後多次點選Click Me按鈕,你可以看到以下輸出:

[譯]使用React.memo()來優化函式元件的效能
我們可以看到'componentWillUpdate'和'componentWillUpdate'在每次我們點選完按鈕後,都會在控制檯輸出來。所以即使count被設定相同的值,TestC元件還是會被重新渲染,這些就是所謂的無用渲染。

Pure Component/shouldComponentUpdate

為了避免React元件的無用渲染,我們可以實現自己的shouldComponentUpdate生命週期函式。

當React想要渲染一個元件的時候,它將會呼叫這個元件的shouldComponentUpdate函式, 這個函式會告訴它是不是真的要渲染這個元件。

如果我們的shouldComponentUpdate函式這樣寫:

shouldComponentUpdate(nextProps, nextState) {
    return true        
}
複製程式碼

其中各個引數的含義是:

  • nextProps: 元件將會接收的下一個引數props
  • nextProps: 元件的下一個狀態state

因為我們的shouldComponentUpdate函式一直返回true,這就告訴React,無論何種情況都要重新渲染該元件。

可是如果我們這麼寫:

shouldComponentUpdate(nextProps, nextState) {
    return false
}
複製程式碼

因為這個方法的返回值是false,所以React永遠都不會重新渲染我們的元件。

因此當你想要React重新渲染你的元件的時候,就在這個方法中返回true,否則返回false。現在讓我們用shouldComponentUpdate重寫之前的TestC元件:

import React from 'react';

class TestC extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
    }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
    }
    
    shouldComponentUpdate(nextProps, nextState) {
        if (this.state.count === nextState.count) {
            return false
        }
        return true
    }
    
    render() {
        return ( 
            <div> 
            { this.state.count } 
            <button onClick = {
                () => this.setState({ count: 1 }) }> Click Me </button> 
            </div>
        );
    }
}

export default TestC;
複製程式碼

我們在TestC元件裡新增了shouldComponentUpdate方法,判斷如果現在狀態的count和下一個狀態的count一樣時,我們返回false,這樣React將不會進行元件的重新渲染,反之,如果它們兩個的值不一樣,就返回true,這樣元件將會重新進行渲染。

再次在瀏覽器中測試我們的元件,剛開始的介面是這樣的:

[譯]使用React.memo()來優化函式元件的效能

這時候,就算我們多次點選Click Me按鈕,也只能看到兩行輸出:

componentWillUpdate
componentDidUpdate 
複製程式碼

因為第二次點選Click Me按鈕後count值一直是1,這樣shouldComponentUpdate一直返回false,所以元件就不再被重新渲染了。

那麼如何驗證後面state的值發生改變,元件還是會被重新渲染呢?我們可以在瀏覽器的React DevTools外掛中直接對TestC元件的狀態進行更改。具體做法是, 在Chrome除錯工具中點選React標籤,在介面左邊選中TestC元件,在介面的右邊就可以看到其狀態state中只有一個鍵count,且其值是1:

[譯]使用React.memo()來優化函式元件的效能
然後讓我們點選count的值1,將其修改為2,然後按Enter鍵:

[譯]使用React.memo()來優化函式元件的效能
你將會看到控制檯有以下輸出:

componentWillUpdate
componentDidUpdate
componentWillUpdate
componentDidUpdate
複製程式碼

state的count被改變了,元件也被重新渲染了。

現在讓我們使用另外一種方法PureComponent來對元件進行優化。

React在v15.5的時候引入了Pure Component元件。React在進行元件更新時,如果發現這個元件是一個PureComponent,它會將元件現在的state和props和其下一個state和props進行淺比較,如果它們的值沒有變化,就不會進行更新。要想讓你的元件成為Pure Component,只需要extends React.PureComponent即可。

讓我們用PureComponent去改寫一下我們的程式碼吧:

import React from 'react';

class TestC extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
    }
    
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
    }
    
    /*shouldComponentUpdate(nextProps, nextState) {
        if (this.state.count === nextState.count) {
            return false
        }
        return true
    }*/
    
    render() {
        return ( 
            <div> 
            { this.state.count } 
            <button onClick = {
                () => this.setState({ count: 1 })
            }> Click Me </button> 
            </div >
        );
    }
}

export default TestC;
複製程式碼

在上面的程式碼中,我將shouldComponentUpdate的程式碼註釋掉了,因為React.PureComponent本身就幫我們實現了一樣的功能。

改完程式碼後,我們重新整理一下瀏覽器,然後多次點選Click Me按鈕看元件被渲染了多少遍:

[譯]使用React.memo()來優化函式元件的效能
由上面的輸出可知,我們的component只在state由0變為1時被重新渲染了,後面都沒有進行渲染。

函式元件

上面我們探討了如何使用PureComponentshouldComponentUpdate的方法優化類元件的效能。雖然類元件是React應用的主要組成部分,不過函式元件(Functional Component)同樣可以被作為React元件使用。

function TestC(props) {
    return (
        <div>
            I am a functional component
        </div>
    )
}
複製程式碼

對於函式元件,它們沒有諸如state的東西去儲存它們本地的狀態(雖然在React Hooks中函式元件可以使用useState去使用狀態), 所以我們不能像在類元件中使用shouldComponentUpdate等生命函式去控制函式元件的重渲染。當然,我們也不能使用extends React.PureComponent了,因為它壓根就不是一個類。

要探討解決方案,讓我們先驗證一下函式元件是不是也有和類元件一樣的無用渲染的問題。

首先我們先將ES6的TestC類轉換為一個函式元件:

import React from 'react';

const TestC = (props) => {
    console.log(`Rendering TestC :` props)
    return ( 
        <div>
            {props.count}
        </div>
    )
}
export default TestC;
// App.js
<TestC count={5} />
複製程式碼

當上面的程式碼初次載入時,控制檯的輸出是:

[譯]使用React.memo()來優化函式元件的效能
同樣,我們可以開啟Chrome的除錯工具,點選React標籤然後選中TestC元件:

[譯]使用React.memo()來優化函式元件的效能
我們可以看到這個元件的引數值是5,讓我們將這個值改為45, 這時候瀏覽器輸出:

[譯]使用React.memo()來優化函式元件的效能
由於count的值改變了,所以該元件也被重新渲染了,控制檯輸出Object{count: 45},讓我們重複設定count的值為45, 然後再看一下控制檯的輸出結果:

[譯]使用React.memo()來優化函式元件的效能
由輸出結果可以看出,即使count的值保持不變,還是45, 該元件還是被重渲染了。

既然函式元件也有無用渲染的問題,我們如何對其進行優化呢?

解決方案: 使用React.memo()

React.memo(...)是React v16.6引進來的新屬性。它的作用和React.PureComponent類似,是用來控制函式元件的重新渲染的。React.memo(...) 其實就是函式元件的React.PureComponent

如何使用React.memo(...)?

React.memo使用起來非常簡單,假設你有以下的函式元件:

const Funcomponent = ()=> {
    return (
        <div>
            Hiya!! I am a Funtional component
        </div>
    )
}
複製程式碼

我們只需將上面的Funcomponent作為引數傳入React.memo中:

const Funcomponent = ()=> {
    return (
        <div>
            Hiya!! I am a Funtional component
        </div>
    )
}
const MemodFuncComponent = React.memo(FunComponent)
複製程式碼

React.memo會返回一個純化(purified)的元件MemoFuncComponent,這個元件將會在JSX標記中渲染出來。當元件的引數props和狀態state發生改變時,React將會檢查前一個狀態和引數是否和下一個狀態和引數是否相同,如果相同,元件將不會被渲染,如果不同,元件將會被重新渲染。

現在讓我們在TestC元件上使用React.memo進行優化:

let TestC = (props) => {
    console.log('Rendering TestC :', props)
    return ( 
        <div>
        { props.count }
        </>
    )
}
TestC = React.memo(TestC);
複製程式碼

開啟瀏覽器重新載入我們的應用。然後開啟Chrome除錯工具,點選React標籤,然後選中<Memo(TestC)>元件。

接著編輯一下props的值,將count改為89,我們將會看到我們的應用被重新渲染了:

[譯]使用React.memo()來優化函式元件的效能
然後重複設定count的值為89:

[譯]使用React.memo()來優化函式元件的效能
這裡沒有重新渲染!

這就是React.memo(...)這個函式牛X的地方!

在我們之前那個沒用到React.memo(...)的例子中,count的重複設定會使元件進行重新渲染。可是我們用了React.memo後,該元件在傳入的值不變的前提下是不會被重新渲染的。

結論

以下是幾點總結:

  • React.PureComponent是銀
  • React.memo(...)是金
  • React.PureComponent是給ES6的類元件使用的
  • React.memo(...)是給函式元件使用的
  • React.PureComponent減少ES6的類元件的無用渲染
  • React.memo(...)減少函式元件的無用渲染
  • 為函式元件提供優化是一個巨大的進步

我是進擊的大蔥,關注我的公眾號,獲取我分享的最新技術推送!

[譯]使用React.memo()來優化函式元件的效能

相關文章