從零開始的 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
orReact.PureComponent
.
不過這 2 個類別行為存在若干差異,如果沒有透徹了解就很容易造成 bug 或者效能上的問題。
本文環境
- React
PureComponent
React 官方文件已清楚地解釋 React.Component
與 React.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;
}
首先, Shallow comparison 會先用 Object.is() 進行數值比較,如果符合以下情況,都會視為 2 個數值相同:
- 都是
undefined
/NaN
/null
- 都是
true
或false
- 都是相同字串(大小寫也要一樣)
- 如果是 object 或 Symbol 則是需要 reference 都相同(記憶體中的參照必須相同)
- 如果是 BigInt 則需要數值(numeric value)相同
- 都是 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