本文簡要介紹了 React 中 PureComponent 與 Component 的區別以及使用時需要注意的問題,並在後面附上了原始碼解析,希望對有疑惑的朋友提供一些幫助。
前言
先介紹一下 PureComponent,平時我們建立 React 元件一般是繼承於 Component,而 PureComponent 相當於是一個更純淨的 Component,對更新前後的資料進行了一次淺比較。只有在資料真正發生改變時,才會對元件重新進行 render。因此可以大大提高元件的效能。
對比 Component 和 PureComponent
繼承 Component 建立元件
App.js
裡面的 state 有兩個屬性,text 屬性是基本資料型別,todo 屬性是引用型別。針對這兩種資料型別分別進行對比:
import React, { Component, PureComponent } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props)
this.state = {
text: 'Hello',
todo: {
id: 1,
message: '學習 React'
}
}
}
/**
* 修改 state 中 text 屬性的函式
*/
changeText = () => {
this.setState({
text: 'World'
});
}
/**
* 修改 state 中 todo 物件的函式
*/
changeTodo = () => {
this.setState({
id: 1,
message: '學習 Vue'
});
}
render() {
// 列印 log,檢視渲染情況
console.log('tag', 'render');
const { text, todo } = this.state;
return (
<div className="App">
<div>
<span>文字:{ text }</span>
<button onClick={ this.changeText }>更改文字</button>
</div>
<br />
<div>
<span>計劃:{ todo.message }</span>
<button onClick={ this.changeTodo }>更改計劃</button>
</div>
</div>
);
}
}
export default App;
複製程式碼
瀏覽器中介面
測試
執行專案,開啟控制檯,此時看到只有一個 log:tag render
-
點選 5 次 ·更改文字· 按鈕,可以看到控制檯再次多列印了 5 次 log,瀏覽器中的
Hello
文字變成了World
-
點選 5 次 ·更改計劃· 按鈕,控制檯一樣多列印 5 次 log,瀏覽器中的
學習 React
計劃變成了學習 Vue
分析一下,其實 5 次點選中只有一次是有效的,後來的資料其實並沒有真正改變,但是由於依然使用了 setState(),所以還是會重新 render。所以這種模式是比較消耗效能的。
繼承 PureComponent
其實 PureComponent 用法也是和 Component 一樣,只不過是將繼承 Component 換成了 PureComponent。
App.js
...
// 上面的程式碼和之前一致
class App extends PureComponent {
// 下面的程式碼也和之前一樣
...
}
export default App;
複製程式碼
瀏覽器中介面
測試
和上面 Component 的測試方式一樣
-
點選 5 次 ·更改文字· 按鈕,可以看到控制檯只多列印了** 1 次** log,瀏覽器中的
Hello
文字變成了World
-
點選 5 次 ·更改計劃· 按鈕,控制檯只多列印了 1 次 log,瀏覽器中的
學習 React
計劃變成了學習 Vue
由此可以看出,使用 PureComponent 還是比較節省效能的,即便是使用了 setState(),也會在資料真正改變時才會重新渲染元件
使用時可能遇到的問題
下面我們將程式碼中 changeText
和 changeTodo
方法修改一下
/**
* 修改 state 中 text 屬性的函式
*/
changeText = () => {
let { text } = this.state;
text = 'World';
this.setState({
text
});
}
/**
* 修改 state 中 todo 物件的函式
*/
changeTodo = () => {
let { todo } = this.state;
todo.message = "學習 Vue";
this.setState({
todo
});
}
複製程式碼
此時我們再重新測試一下:
-
點選 ·更改文字· 按鈕,控制檯多列印一次 log,瀏覽器中的
Hello
文字變成了World
-
**注意:**點選 ·更改計劃· 按鈕,控制檯沒有 log 列印,瀏覽器中的計劃也沒有發生改變
為什麼程式碼修改之後,明明 todo 裡的 message 屬性也已經發生變化了,呼叫 setState(),卻不進行渲染了呢?這是因為 PureComponent 在呼叫 shouldComponent 生命週期的時候,對資料進行了一次淺比較,判斷資料是否發生改變,沒發生改變,返回 false,改變了,就返回 true。那這個淺比較的機制是怎麼做的呢?我們一起看下面原始碼解析,來分析一下。
PureComponent 原始碼解析
ReactBaseClasses.js (Github 程式碼位置)
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
/**
* Convenience component with default shallow equality check for sCU.
*/
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
複製程式碼
可以看到 PureComponent 的使用和 Component 一致,只時最後為其新增了一個 isPureReactComponent 屬性。ComponentDummy 就是通過原型模擬繼承的方式將 Component 原型中的方法和屬性傳遞給了 PureComponent。同時為了避免原型鏈拉長導致屬性查詢的效能消耗,通過 Object.assign 把屬性從 Component 拷貝了過來。
但是這裡只是 PureComponent 的宣告建立,沒有顯示如何進行比較更新的,那我們繼續看下面的程式碼。
ReactFiberClassComponent.js (Github 程式碼位置)
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
...
// 這裡根據上面 PureComponent 設定的屬性 isPureReactComponent 判斷一下,如果是 PureComponent,就會走裡面的程式碼,將比較的值返回出去
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
}
複製程式碼
shallowEqual 是在 share
包中一個工具方法,看一下其中的內部實現吧。
shallowEqual.js (Github 程式碼位置)
import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
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++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
複製程式碼
這裡面還呼叫了 is 函式,這個函式也是 share 包中的一個工具方法。
objectIs.js (Github 程式碼位置)
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
export default is;
複製程式碼
PureComponent原始碼分析總結
由上面的原始碼可以發現,其實 PureComponent 和 Component 中的方法和屬性基本一致,只不過 PureComponent 多了一個 isPureReactComponent 為 true 的屬性。在 checkShouldComponentUpdate 的時候,會根據這個屬性判斷是否是 PureComponent,如果是的話,就會根據 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) 這個判斷語句的返回值作為更新依據。所以,檢視了 shallowEqual 和 objectIs 的檔案原始碼,我們可以得出 PureComponent 的淺比較結論:
-
先通過 is 函式判斷兩個引數是否相同,相同則直接返回 ture,也就是不更新元件。
- 根據 objectIs.js 程式碼可知,基本屬性型別判斷值是否相同(包括 NaN),引用資料型別判斷是否是一個引用
-
若 is 函式判斷為 false,則判斷兩個引數是否都為 物件 且 都不為 null,若任意一個 不是物件 或 任意一個為 null,直接返回 false,也就是更新元件
-
若前兩個判斷都通過,則可斷定兩個引數皆為物件,此時判斷它們 keys 的長度是否相同,若不同,則直接返回 false,即更新元件
-
若 keys 長度不同,則對兩個物件中的第一層屬性進行比較,若都相同,則返回 true,有任一屬性不同,則返回 false
總結
閱讀原始碼之後,可以發現之前我們修改了 changeTodo 方法的邏輯之後,為什麼資料改變,元件卻依然不更新的原因了。是因為修改的是同一個物件,所以 PureComponent 預設引用相同,不進行元件更新,所以才會出現這個陷阱,在使用的過程中希望大家注意一下這個問題。
-
對比 PureComponent 和 Component,可以發現,PureComponent 效能更高,一般有幾次有效修改,就會進行幾次有效更新
-
為了避免出現上面所說的陷阱問題,建議將 React 和 Immutable.js 配合使用,因為 Immutable.js 中的資料型別都是不可變,每個變數都不會相同。但是由於 Immutable 學習成本較高,可以在專案中使用 immutability-helper 外掛,也能實現類似的功能。關於 immutability-helper 的使用,可以檢視我的另一篇部落格:immutability-helper 外掛的基本使用
-
雖然 PureComponent 提高了效能,但是也只是對資料進行了一次淺比較,最能優化效能的方式還是自己在 shouldComponent 生命週期中實現響應邏輯
-
關於 PureComponent 淺比較的總結可以檢視上面的PureComponent 原始碼分析總結