React前端渲染優化–父組件導致子組件重復渲染的問題

React前端渲染優化–父組件導致子組件重復渲染

說明

目前我們所使用 react 版本一般會有以下四種方式觸發渲染 render,而其中通過父組件 render 會直接通知子組件也進行 render。

一般的優化方式

鑒於此種情況,如果完全不做控制下,父組件 render, 那麼子組件一定會 render。真實 dom 的渲染 react 會在 diff 算法之後合計出最小改動,進行操作。但對於結構復雜頁面,自頂向下,隻是單純 diff 也要花費很長的時間來處理 js 任務。再加上我們每個組件的 render 中也會寫很多業務、數據處理。

js 為單線程執行,顯然,不必要的子組件的 render 會浪費 js 線程資源,復雜任務還會長時間占用線程導致假死狀態,也就是頁面卡頓,react 底層有 Fiber 來優化任務隊列,但無法優化業務代碼上的問題。

一般子組件可以通過確認 props 是否發生變化來控制自身是否進行 render,比如 react-mobx 中的 observer 高階方法或者 React.PureComponet 就是用來做淺層比較進行控制處理。

項目中常見會導致重復渲染的寫法以及改進方法

函數導致的渲染重復

箭頭函數 props.fn = () => {} 或者 綁定方法 props.fn = this.xxx.bind(this)

這樣的寫法每次父組件 render 都會新聲明一個 function 傳遞給子組件,會導致 observer 失去比對作用,父組件每次 render 都會使這個組件 render,嚴重影響性能!

import React from 'react';
import { observer } from 'mobx-react';
// 我們開發中常見的一個被觀測組件,例如 ObserverComponent
@observer
class ObserverComponent extends React.Component {
    render() {
        return (<div>ObserverComponent</div>)
    }
}
// 例如在父組件 Parent 使用被觀測的子組件 ObserverComponent
// 請不要給子組件 ObserverComponent 的 props 設置 箭頭函數 () => {} 或者 fn.bind(this) 方法
@observer
class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this); // 【正確】
    }
    handleChange() {}
    doSomething = () => {}
    render() {
        return (
            <ObserverComponent
                onChange={() => {}}                      // 【錯誤】
                onChange={this.handleChange.bind(this)}  // 【錯誤】
                onChange={this.handleChange}             // 【正確】
                todo={this.doSomething}                  // 【正確】
            />
        )
    }
}

字面量寫法導致的渲染重復

由於字面量的寫法{} 和 { pageSizeOptions: ['10'] },每次都會字面量聲明一個新的對象傳遞給列表組件,導致頁面重新 render。

toJS() 方法每次也會返回新對象,會導致頁面重新渲染

組件重復渲染問題(pureComponent, React.memo, useMemo, useCallback)

在一個組件中, 其state變化會引起render的重新執行, 函數式組件中, 使用setHook更新state也會引起render的重新執行

render執行會帶來兩個方面的影響

  • 1.當前組件需要重新渲染, 除瞭那些狀態和生命周期初始化被保留的,其餘正常的都會重新執行。
  • 2.子組件會重新渲染, 即使其是一個無狀態組件

針對上述問題, react給出來解決方案:

  • pureComponent
  • React.memo
  • useMemo
  • useCallback

下面將具體說明這幾個都使用場景和解決的問題

  • useMemo設計的初衷就是避免重復進行大規模的計算, 它的理想作用對象是當前組件

具體是將當前組件中一個經過很復雜的計算得到的值緩存起來, 當其依賴項不變的時候, 即使組件重新渲染, 也不會重新計算。

通過上述描述也能理解出其緩存的是一個具體的數據(可以和接下來的useCallback區分開)

/*
緩存瞭一個對象, 隻有當count變化時才會重新返回該對象
*/
const useInfo = useMemo(
    () => ({
        count: count,
        name: "name"
    }),
    [count]
)
  • 針對第二點, 分別有三個解決方案

首先是useCallback, 其語法和useMemo基本一致, 但是其使用場景是父組件定義瞭一個函數並且將這個函數傳遞給瞭子組件, 那麼當父組件重新渲染時,生成的會是一個新的函數, 這個時候就可以使用useCallback瞭,如下:

const Page = (props) => {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('Child組件');
    return (
        <>
            <ChildMemo name={name} onClick={ useCallback((newName: string) => setName(newName), []) }/>
            {/* useCallback((newName: string) => setName(newName),[]) */}
            {/* 這裡使用瞭useCallback優化瞭傳遞給子組件的函數,隻初始化一次這個函數,下次不產生新的函數
        </>
    )
}

上述是一個簡寫的形式,意思就是將傳遞給子組件的這個函數緩存瞭,其第二個參數就是依賴,當該依賴變化時,將會重新緩存該函數

其餘useMemo的區別就在於,其緩存的是函數本身,而useMemo緩存的是函數計算後的值,都會在依賴項變化時重新緩存。

註:雖然其可能對於父組件傳遞給子組件函數時可能很理想,但實際上其帶來的性能損耗也是顯而易見的,其使用場景不應該是擔心本組件的函數因為本組件重新渲染而重新生成,這樣反而起到瞭反效果,當前組件更新,其重新渲染,內部的函數也重新生成,其性能損耗可以忽略不計,如下圖。

使用場景應該是父組件更新導致重新生成的函數又傳遞給瞭子組件,導致子組件重新渲染。

  • 接著是pureComponent

它是一個類, 組件繼承自它後, 其作為子組件時, 每次父組件更新後, 會淺對比傳來的props是否變化, 若沒變化, 則子組件不更新。

  • React.memo

同上條功能類似, 當其作用於函數式組件並且作為子組件時, 每次父組件更新後, 會淺對比傳來的props是否變化, 若沒變化, 則子組件不更新。

// 子組件暴露時暴露為處理後的組件
import {memo} from 'react'
const TeacherModal = (props: any) => {
  return <div></div>
}
export default memo(TeacherModal)

上面兩個都區別在於, 一個是類, 一個是高階組件, 前者作用於類後者作用於函數

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: