shouldComponentUpdate 的作用
在一個組件的子樹中,每個節點中,SCU 代表 shouldComponentUpdate 返回的值,而 vDOMEq 代表返回的 React 元素是否相同。最后,圓圈的顏色代表了該組件是否需要被調停。
節點 C2 的 shouldComponentUpdate 返回了 false,React 因而不會去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate 不會被調用到。
對於 C1 和 C3,shouldComponentUpdate 返回了 true,所以 React 需要繼續向下查詢子節點。這里 C6 的 shouldComponentUpdate 返回了 true,同時由於渲染的元素與之前的不同使得 React 更新了該 DOM。
最后一個有趣的例子是 C8。React 需要渲染這個組件,但是由於其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。
顯而易見,你看到 React 只改變了 C6 的 DOM。對於 C8,通過對比了渲染的 React 元素跳過了渲染。而對於 C2 的子節點和 C7,由於 shouldComponentUpdate 使得 render 並沒有被調用。因此它們也不需要對比元素了。
示例
如果你的組件只有當 props.color 或者 state.count 的值改變才需要更新時,你可以使用 shouldComponentUpdate 來進行檢查:
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
在這段代碼中,shouldComponentUpdate 僅檢查了 props.color 或 state.count 是否改變。如果這些值沒有改變,那么這個組件不會更新。如果你的組件更復雜一些,你可以使用類似“淺比較”的模式來檢查 props 和 state 中所有的字段,以此來決定是否組件需要更新。React 已經提供了一位好幫手來幫你實現這種常見的模式 - 你只要繼承 React.PureComponent 就行了。所以這段代碼可以改成以下這種更簡潔的形式:
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
大部分情況下,你可以使用 React.PureComponent 來代替手寫 shouldComponentUpdate。但它只進行淺比較,所以當 props 或者 state 某種程度是可變的話,淺比較會有遺漏,那你就不能使用它了。當數據結構很復雜時,情況會變得麻煩。例如,你想要一個 ListOfWords 組件來渲染一組用逗號分開的單詞。它有一個叫做 WordAdder 的父組件,該組件允許你點擊一個按鈕來添加一個單詞到列表中。以下代碼並不正確:
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 這部分代碼很糟,而且還有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
不可變數據的力量
避免該問題最簡單的方式是避免更改你正用於 props 或 state 的值。例如,上面 handleClick 方法可以用 concat 重寫:
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
ES6 數組支持擴展運算符,這讓代碼寫起來更方便了。如果你在使用 Create React App,該語法已經默認支持了。
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
你可以用類似的方式改寫代碼來避免可變對象的產生。例如,我們有一個叫做 colormap 的對象。我們希望寫一個方法來將 colormap.right 設置為 'blue'。我們可以這么寫:
function updateColorMap(colormap) {
colormap.right = 'blue';
}
為了不改變原本的對象,我們可以使用 Object.assign 方法:
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
現在 updateColorMap 返回了一個新的對象,而不是修改老對象。Object.assign 是 ES6 的方法,需要 polyfill。
這里有一個 JavaScript 的提案,旨在添加對象擴展屬性以使得更新不可變對象變得更方便:
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
如果你在使用 Create React App,Object.assign 以及對象擴展運算符已經默認支持了。
使用不可變數據結構
Immutable.js 是另一種解決方案。它通過結構共享提供了不可變、持久化集合:
- 不可變:一旦創建,一個集合便不能再被修改。
- 持久化:對集合進行修改,會創建一個新的集合。之前的集合仍然有效。
- 結構共享:新的集合會盡可能復用之前集合的結構,以最小化拷貝操作來提高性能。
不可變數據使得追蹤變更非常容易。每次變更都會生成一個新的對象使得我們只需要檢查對象的引用是否改變。舉個例子,這是一段很常見的 JavaScript 代碼:
const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true
由於 y 被指向和 x 相同的對象,雖然我們修改了 y,但是對比結果還是 true。你可以使用 immutable.js 來寫相似的代碼:
const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
const z = x.set('foo', 'bar');
x === y; // false
x === z; // true
在這個例子中,修改 x 后我們得到了一個新的引用,我們可以通過判斷引用 (x === y) 來驗證 y 中存的值和原本 x 中存的值不同。
還有其他可以幫助實現不可變數據的庫,分別是 Immer, immutability-helper 以及 seamless-immutable。
不可變數據結構使你可以方便地追蹤對象的變化,這是應用 shouldComponentUpdate 所需要的。讓性能得以提升。