從零開始的 React 教學 Part 5 - PureComponent

Posted on  Oct 5, 2022  in  ReactJS 前端框架  by  Amo Chen  ‐ 4 min read

從零開始的 React 教學 Part 3 - Class Components 一文介紹 Class Components, React 讓開發者們透過繼承 React.Component 類別並且實作少數方法就能夠實作 React 元件(component)。

但除了 React.Component , React 還提供另外 1 個 React.PureComponent 類別,開發者們也可以選擇繼承該類別實作 React 元件。

React components can be defined by subclassing React.Component or React.PureComponent.

不過這 2 個類別行為存在若干差異,如果沒有透徹了解就很容易造成 bug 或者效能上的問題。

本文環境

  • React

PureComponent

React 官方文件已清楚地解釋 React.ComponentReact.PureComponent 之間的差異:

React.PureComponent 預設已經實作 shouldComponentUpdate() 方法,而 React.Component 沒有。

不過 React.PureComponent 實作 shouldComponentUpdate() 方法使用的是 shallow comparison 對 prop 與 state 進行比較,以藉此判斷是否要重新渲染(render)元件。

React.PureComponent is similar to React.Component . The difference between them is that React.Component doesn’t implement shouldComponentUpdate() , but React.PureComponent implements it with a shallow prop and state comparison.

因此,學習 React.PureComponent 時,一定要知道何謂 shallow comparison , 否則就容易誤用甚至造成 bug 。

Shallow comparison

關於 Shallow comparison 如何比較變動前與變動後的 prop 或 state, 可以參考以下這段 React 原始碼:

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

引用自 shallowEqual source code

首先, Shallow comparison 會先用 Object.is() 進行數值比較,如果符合以下情況,都會視為 2 個數值相同:

  1. 都是 undefined / NaN / null
  2. 都是 truefalse
  3. 都是相同字串(大小寫也要一樣)
  4. 如果是 object 或 Symbol 則是需要 reference 都相同(記憶體中的參照必須相同)
  5. 如果是 BigInt 則需要數值(numeric value)相同
  6. 都是 number 的情況下則是需要數值相同,正負號也要相同,所以 +0-0 會被判定為不同

以下是詳細範例可以參考(引用自 MDN ),如果想實際操作的話,可以打開瀏覽器開發者工具,並在 console 輸入以下程式碼試看看:

// Case 1: Evaluation result is the same as using ===
Object.is(25, 25); // true
Object.is("foo", "foo"); // true
Object.is("foo", "bar"); // false
Object.is(null, null); // true
Object.is(undefined, undefined); // true
Object.is(window, window); // true
Object.is([], []); // false
const foo = { a: 1 };
const bar = { a: 1 };
const sameFoo = foo;
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(foo, sameFoo); // true

// Case 2: Signed zero
Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(-0, -0); // true

// Case 3: NaN
Object.is(NaN, 0 / 0); // true
Object.is(NaN, Number.NaN); // true

接著,如果 Object.is() 回傳 false ,就代表 2 個值可能都是 object, 因此會再進一步比較 object 第 1 層 key 的部分,這邊十分值得注意,這也是當 prop 或 state 是巢狀(nested)的 objects 就不適合使用 React.PureComponent 的原因,因為 shallow comparison 只有比較第1 層 key 的部分,當第 2 層之後的 prop 或 state 有變動,極有可能會被 shallow comparison 忽略判定相等,導致元件沒有進行重新渲染。

舉下列 React 程式為例,當按下按鈕更新 App 元件內的 value 的值後, Pure 元件卻沒跟著更新,這是因為 PureComponent 只是單純比較 Object 第 1 層 key 的 reference 而已,這就是 shallow comparison 造成的問題:

import React from 'react';
import ReactDOM from 'react-dom/client';

class Pure extends React.PureComponent {
  render() {
    return (
      <div>
        PureComponent get "b" value: {this.props.value.a.b}
      </div>
    )
  }
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.value = {
      a: {
        b: 1
      }
    }
  }

  updateValue = () => {
    this.value.a.b += 1
    this.forceUpdate()
  }

  render() {
    return (
      <div>
        <h1>PureComponent Test</h1>
        <div>Current "b" value: {this.value.a.b}</div>
        <Pure value={this.value} />
        <div>
          <button onClick={this.updateValue}>Update value</button>
        </div>
      </div>
    )
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

p.s. 上述範例的 this.value 僅是為了舉例而使用,較好的方式是放到 this.state

上述範例可於 GitHub 查看,成功執行的畫面如下所示,可以發現 Pure 元件一直沒有重新渲染:

如果要修好上述提到的情況,也只需要將 Pure extends React.PureComponent 改為 Pure extends React.Component 即可,或者明確實作 Pure 元件的 shouldComponentUpdate() 方法。

另外,由於 shallow comparison 是比較 object / array / Symbol 的 reference, 所以在值沒有任何改變,但 reference 一直改變的情況下,例如每次都建立值相同的全新 object / array / Symbol,就會造成 React.PureComponent 一直重新渲染,造成 React 效能不佳。

舉下列程式為例,該範例在 App 元件的 render() 內每次進行渲染時都建立一個全新的 constValue , 並且將之傳給 Pure 元件,即使該值沒有任何變動,卻導致每次 App 元件渲染時, Pure 元件也跟著重新渲染:

import React from 'react';
import ReactDOM from 'react-dom/client';

class Pure extends React.PureComponent {
  render() {
    console.log('render Pure')
    return (
      <div>
        PureComponent get value: {JSON.stringify(this.props.value)}
      </div>
    )
  }
}

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
    }
  }

  updateCount = () => {
    const count = this.state.count + 1
    this.setState({count: count})
  }

  render() {
    const constValue = {
      a: {
        b: 1
      }
    };
    return (
      <div>
        <h1>PureComponent Test</h1>
        <div>Current value: {JSON.stringify(constValue)}</div>
        <Pure value={constValue} />
        <div>
          <button onClick={this.updateCount}>Update value</button>
        </div>
      </div>
    )
  }
}

const domContainer = document.querySelector('#app');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);

上述範例可於 GitHub 查看,成功執行的畫面如下所示,可以發現 console 一直顯示 Pure 元件一直在重新渲染:

如果要修好上述提到的情況,也只需要將 constValue 移出 render() 之外,不要每次都重新建立即可,或者也可以實作 Pure 元件的shouldComponentUpdate() 方法。

另外,也要小心以下這種寫法:

<PureComponent>
  <Component />
</PureComponent>

因為上述的寫法等同於下列形式,每次 render 都會建立一個新的 <Component /> 造成 PureComponent 重新渲染:

<PureComponent children={<Component />} />

什麼時候使用 PureComponent?

使用 shallow comparison 為 PureComponent 帶來的好處是比較的速度快,可以節省不必要的渲染,帶來整體渲染速度的提升。

所以對於較為簡單 props 或 state, 例如都是單純的字串、數值、僅有一層的 object 等,可以使用 React.PureComponent ,或是一些不常變動的元件也可以選擇繼承 React.PureComponent 進行實作。

另外 React 官網也提及針對巢狀(nested)的資料,可以考慮使用 immutable objects 來提升比較的速度。

Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

以上就是關於 React.PureComponent 的介紹。

Happy Coding!

References

https://reactjs.org/docs/react-api.html

https://github.com/facebook/react/blob/55d75005bc26aa41cddc090273f82aa106729fb8/packages/shared/shallowEqual.js

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!