在談性能優(yōu)化之前,先拋出一個(gè)問(wèn)題:
一個(gè) React 組件,它包含兩個(gè)子組件,分別是函數(shù)組件和 Class 組件。當(dāng)這個(gè) React 組件的 state 發(fā)生變化時(shí),兩個(gè)子組件的 props 并沒(méi)有發(fā)生變化,此時(shí)是否會(huì)導(dǎo)致函數(shù)子組件和 Class 子組件發(fā)生重復(fù)渲染呢?
曾拿這個(gè)問(wèn)題問(wèn)過(guò)不少前端求職者,但很少能給出正確的答案。下面就這個(gè)問(wèn)題,淺談下自己的認(rèn)識(shí)。
一、場(chǎng)景復(fù)現(xiàn)
針對(duì)上述問(wèn)題,先進(jìn)行一個(gè)簡(jiǎn)單的復(fù)現(xiàn)驗(yàn)證。
App 組件包含兩個(gè)子組件,分別是函數(shù)組件 ChildFunc 和類(lèi)組件 ChildClass。App 組件每隔 2 秒會(huì)對(duì)自身狀態(tài) cnt 自行累加 1,用于驗(yàn)證兩個(gè)子組件是否會(huì)發(fā)生重復(fù)渲染,具體代碼邏輯如下。
App 組件:
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
import ChildClass from './ChildClass.jsx';
import ChildFunc from './ChildFunc.jsx';
class App extends Component {
state = {
cnt: 1
};
componentDidMount() {
setInterval(() => this.setState({ cnt: this.state.cnt + 1 }), 2000);
}
render() {
return (
<Fragment>
<h2>疑問(wèn):</h2>
<p>
一個(gè) React 組件,它包含兩個(gè)子組件,分別是函數(shù)組件和 Class 組件。當(dāng)這個(gè) React 組件的 state
發(fā)生變化時(shí),兩個(gè)子組件的 props 并沒(méi)有發(fā)生變化,此時(shí)是否會(huì)導(dǎo)致函數(shù)子組件和 Class 子組件發(fā)生重復(fù)渲染呢?
</p>
<div>
<h3>驗(yàn)證(性能優(yōu)化前):</h3>
<ChildFunc />
<ChildClass />
</div>
</Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
Class 組件:
import React, { Component } from 'react';
let cnt = 0;
class ChildClass extends Component {
render() {
cnt = cnt + 1;
return <p>Class組件發(fā)生渲染次數(shù): {cnt}</p>;
}
}
export default ChildClass;
函數(shù)組件:
import React from 'react';
let cnt = 0;
const ChildFunc = () => {
cnt = cnt + 1;
return <p>函數(shù)組件發(fā)生渲染次數(shù): {cnt}</p>;
};
export default ChildFunc;
實(shí)際驗(yàn)證結(jié)果表明,如下圖所示,無(wú)論是函數(shù)組件還是 Class 組件,只要父組件的 state 發(fā)生了變化,二者均會(huì)產(chǎn)生重復(fù)渲染。
二、性能優(yōu)化
那么該如何減少子組件發(fā)生重復(fù)渲染呢?好在 React 官方提供了 memo
組件和PureComponent
組件分別用于減少函數(shù)組件和類(lèi)組件的重復(fù)渲染,具體優(yōu)化邏輯如下:
Class 組件:
import React, { PureComponent } from 'react';
let cnt = 0;
class ChildClass extends PureComponent {
render() {
cnt = cnt + 1;
return <p>Class組件發(fā)生渲染次數(shù): {cnt}</p>;
}
}
export default ChildClass;
函數(shù)組件:
import React, { memo } from 'react';
let cnt = 0;
const OpChildFunc = () => {
cnt = cnt + 1;
return <p>函數(shù)組件發(fā)生渲染次數(shù): {cnt}</p>;
};
export default memo(OpChildFunc);
實(shí)際驗(yàn)證結(jié)果如下圖所示,每當(dāng) App 組件狀態(tài)發(fā)生變化時(shí),優(yōu)化后的函數(shù)子組件和類(lèi)子組件均不再產(chǎn)生重復(fù)渲染。
下面結(jié)合 React 源碼,淺談下 PureComponent 組件和 memo 組件的實(shí)現(xiàn)原理。
三、PureComponent 組件
3.1 PureComponent 概念
以下內(nèi)容摘自React.PureComponent。
React.PureComponent
與 React.Component
很相似。兩者的區(qū)別在于 React.Component
并未實(shí)現(xiàn) shouldComponentUpdate()
,而 React.PureComponent
中以淺層對(duì)比 prop 和 state 的方式來(lái)實(shí)現(xiàn)了該函數(shù)。
如果賦予 React 組件相同的 props 和 state,render() 函數(shù)會(huì)渲染相同的內(nèi)容,那么在某些情況下使用 React.PureComponent 可提高性能。
注意:
React.PureComponent 中的 shouldComponentUpdate() 僅作對(duì)象的淺層比較。如果對(duì)象中包含復(fù)雜的數(shù)據(jù)結(jié)構(gòu),則有可能因?yàn)闊o(wú)法檢查深層的差別,產(chǎn)生錯(cuò)誤的比對(duì)結(jié)果。僅在你的 props 和 state 較為簡(jiǎn)單時(shí),才使用 React.PureComponent,或者在深層數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時(shí)調(diào)用 forceUpdate() 來(lái)確保組件被正確地更新。你也可以考慮使用 immutable 對(duì)象加速嵌套數(shù)據(jù)的比較。
此外,React.PureComponent 中的 shouldComponentUpdate() 將跳過(guò)所有子組件樹(shù)的 prop 更新。因此,請(qǐng)確保所有子組件也都是“純”的組件。
3.2 PureComponent 性能優(yōu)化實(shí)現(xiàn)機(jī)制
3.2.1 PureComponent 組件定義
以下代碼摘自 React v16.9.0 中的 ReactBaseClasses.js
文件。
// ComponentDummy起橋接作用,用于PureComponent實(shí)現(xiàn)一個(gè)正確的原型鏈,其原型指向Component.prototype
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
// 定義PureComponent構(gòu)造函數(shù)
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;
}
// 將PureComponent的原型指向一個(gè)新的對(duì)象,該對(duì)象的原型正好指向Component.prototype
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
// 將PureComponent原型的構(gòu)造函數(shù)修復(fù)為PureComponent
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
// 創(chuàng)建標(biāo)識(shí)isPureReactComponent,用于標(biāo)記是否是PureComponent
pureComponentPrototype.isPureReactComponent = true;
3.2.2 PureComponent 組件的性能優(yōu)化實(shí)現(xiàn)機(jī)制
名詞解釋?zhuān)?/strong>
- work-in-progress(簡(jiǎn)寫(xiě) WIP: 半成品):表示尚未完成的 Fiber,也就是尚未返回的堆棧幀,對(duì)象 workInProgress 是 reconcile 過(guò)程中從 Fiber 建立的當(dāng)前進(jìn)度快照,用于斷點(diǎn)恢復(fù)。
以下代碼摘自 React v16.9.0 中的 ReactFiberClassComponent.js
文件。
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
// 如果這個(gè)組件實(shí)例自定義了shouldComponentUpdate生命周期函數(shù)
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
// 執(zhí)行這個(gè)組件實(shí)例自定義的shouldComponentUpdate生命周期函數(shù)
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
stopPhaseTimer();
return shouldUpdate;
}
// 判斷當(dāng)前組件實(shí)例是否是PureReactComponent
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
/**
* 1. 淺比較判斷 oldProps 與newProps 是否相等;
* 2. 淺比較判斷 oldState 與newState 是否相等;
*/
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
由上述代碼可以看出,如果一個(gè) PureComponent 組件自定義了shouldComponentUpdate
生命周期函數(shù),則該組件是否進(jìn)行渲染取決于shouldComponentUpdate
生命周期函數(shù)的執(zhí)行結(jié)果,不會(huì)再進(jìn)行額外的淺比較。如果未定義該生命周期函數(shù),才會(huì)淺比較狀態(tài) state 和 props。
四、memo 組件
4.1 React.memo 概念
以下內(nèi)容摘自React.memo。
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
React.memo
為高階組件。它與React.PureComponent
非常相似,但它適用于函數(shù)組件,但不適用于 class 組件。
如果你的函數(shù)組件在給定相同props
的情況下渲染相同的結(jié)果,那么你可以通過(guò)將其包裝在React.memo
中調(diào)用,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。
默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比,如果你想要控制對(duì)比過(guò)程,那么請(qǐng)將自定義的比較函數(shù)通過(guò)第二個(gè)參數(shù)傳入來(lái)實(shí)現(xiàn)。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 傳入 render 方法的返回結(jié)果與
將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true,
否則返回 false
*/
}
export default React.memo(MyComponent, areEqual);
此方法僅作為性能優(yōu)化的方式而存在。但請(qǐng)不要依賴(lài)它來(lái)“阻止”渲染,因?yàn)檫@會(huì)產(chǎn)生 bug。
注意
與 class 組件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 會(huì)返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。
4.2 React.memo 性能優(yōu)化實(shí)現(xiàn)機(jī)制
4.2.1 memo 函數(shù)定義
我們先看下在 React 中 memo 函數(shù)是如何定義的,以下代碼摘自 React v16.9.0 中的memo.js
文件。
export default function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
return {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
}
其中:
- type:表示自定義的 React 組件;
- compare:表示自定義的性能優(yōu)化函數(shù),類(lèi)似
shouldcomponentupdate
生命周期函數(shù);
4.2.2 memo 函數(shù)的性能優(yōu)化實(shí)現(xiàn)機(jī)制
以下代碼摘自 React v16.9.0 中的 ReactFiberBeginWork.js
文件。
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
): null | Fiber {
/* ...省略...*/
// 判斷更新的過(guò)期時(shí)間是否小于渲染的過(guò)期時(shí)間
if (updateExpirationTime < renderExpirationTime) {
const prevProps = currentChild.memoizedProps;
// 如果自定義了compare函數(shù),則采用自定義的compare函數(shù),否則采用官方的shallowEqual(淺比較)函數(shù)。
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
/**
* 1. 判斷當(dāng)前 props 與 nextProps 是否相等;
* 2. 判斷即將渲染組件的引用是否與workInProgress Fiber中的引用是否一致;
*
* 只有兩者都為真,才會(huì)退出渲染。
*/
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
// 如果都為真,則退出渲染
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
/* ...省略...*/
}
由上述代碼可以看出,updateMemoComponent
函數(shù)決定是否退出渲染取決于以下兩點(diǎn):
- 當(dāng)前 props 與 nextProps 是否相等;
- 即將渲染組件的引用是否與 workInProgress Fiber 中的引用是否一致;
只有二者都為真,才會(huì)退出渲染。
其他: