新聞中心
寫在前面
React 提供的虛擬 DOM 和高效的 Diff 算法的完美搭配,實(shí)現(xiàn)了對(duì) DOM 最小粒度的更新。在大多數(shù)情況下,React 對(duì) DOM 的渲染效率可以滿足我們的開發(fā)需求。但是個(gè)別復(fù)雜業(yè)務(wù)場(chǎng)景下,性能問題在所難免,我們需要采取一些措施來提升性能,其中 React 組件的渲染性能優(yōu)化很重要的一點(diǎn)就是避免不必要的渲染。

render 做了什么事情
- Diffing
React 將 render 函數(shù)返回的虛擬 DOM 樹與老的進(jìn)行比較,從而確定 DOM 要不要更新、怎么更新。盡管React使用高度優(yōu)化的算法進(jìn)行 diff ,但是這個(gè)過程仍然會(huì)損耗性能。比方說在 DOM 樹很大的說話,遍歷兩棵樹進(jìn)行各種比對(duì)還是相當(dāng)耗性能的,特別是在頂層 setState 一個(gè)微小的修改,默認(rèn)會(huì)去遍歷整棵樹,然而 JQuery 一行代碼就可以搞定。
對(duì)比可能挫一點(diǎn)的手動(dòng)操作 DOM,diff 上的性能損耗讓 React 贏不了。換句話說 React 提供了一套方便的 DOM 更新機(jī)制,非常方便性能也 OK。
- Reconciliation
根據(jù) diff 的結(jié)果來更新 DOM 樹,來掛載或卸載DOM節(jié)點(diǎn),同樣會(huì)損耗性能,這部分不做過多闡述,感興趣的同學(xué)可以移步文檔 reconciliation 。
什么時(shí)候 render 會(huì)被調(diào)用
- 組件掛載的時(shí)候
React 組件構(gòu)建并將 DOM 元素插入頁面的過程稱為掛載。
setState方法被調(diào)用的時(shí)候
但是執(zhí)行 setState 的時(shí)候一定會(huì)重新渲染嗎?答案是不一定。當(dāng) setState 傳入 null 的時(shí)候,并不會(huì)觸發(fā) render ,不信的同學(xué)可以試一下下面的 demo
- class App extends React.Component {
- state = {
- a: 1
- };
- render() {
- console.log("render");
- return (
- <>
{this.state.a}
- >
- );
- }
- }
- 父組件重新渲染
只要父組件重新渲染了,即使傳入子組件的 props 未發(fā)生變化,那么子組件也會(huì)重新渲染。
我們對(duì)上面的 demo 進(jìn)行稍微的修改,可以看出當(dāng)點(diǎn)擊按鈕的時(shí)候, Child 組件的 props 并沒有發(fā)生變化,但是也觸發(fā)了 render 方法。
- const Child = () => {
- console.log("child render");
- return
child;- };
- class App extends React.Component {
- state = {
- a: 1
- };
- render() {
- console.log("render");
- return (
- <>
{this.state.a}
- >
- );
- }
- }
我們能做什么?
上文描述的 React 組件渲染機(jī)制其實(shí)是一種較好的做法,很好地避免了在每一次狀態(tài)更新之后,需要去手動(dòng)執(zhí)行重新渲染的相關(guān)操作。魚和熊掌不可兼得,帶來方便的同時(shí)也會(huì)存在一些問題,當(dāng)子組件過多或者組件的層級(jí)嵌套過深時(shí),因?yàn)榉捶磸?fù)復(fù)重新渲染狀態(tài)沒有改變的組件,可能會(huì)增加渲染時(shí)間又會(huì)影響用戶體驗(yàn),此時(shí)就需要對(duì) React 的 render 進(jìn)行優(yōu)化。
上面說了不必要的 render 會(huì)帶來性能問題,因此我們的主要優(yōu)化思路就是減少不必要的 render。
在 React 類組件中,利用 shouldComponentUpdate 或者 PureComponent 來減少因?yàn)楦附M件更新而觸發(fā)子組件的render,從而達(dá)到目的。
shouldComponentUpdate 來決定是否組件是否重新渲染,如果不希望組件重新渲染,返回 false 即可。
你真的了解 PureComponent 嗎?
在 React 中 PureComponet 的源碼為
- if (this._compositeType === CompositeTypes.PureClass) {
- shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
- }
看函數(shù)名就能夠理解,PureComponet 通過對(duì) props 和 state的淺比較結(jié)果來實(shí)現(xiàn) shouldComponentUpdate,當(dāng)對(duì)象包含復(fù)雜的數(shù)據(jù)結(jié)構(gòu)時(shí),可能就不靈了,對(duì)象深層的數(shù)據(jù)已改變卻沒有觸發(fā) render。
看到這里,順便看一下 shallowEqual 是如何實(shí)現(xiàn)的。
- const hasOwnProperty = Object.prototype.hasOwnProperty;
- /**
- * is 方法來判斷兩個(gè)值是否是相等的值,為何這么寫可以移步 MDN 的文檔,本文不做過多的闡述
- * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
- */
- function is(x: mixed, y: mixed): boolean {
- if (x === y) {
- return x !== 0 || y !== 0 || 1 / x === 1 / y;
- } else {
- return x !== x && y !== y;
- }
- }
- /**
- *
- */
- function shallowEqual(objA: mixed, objB: mixed): boolean {
- // 首先對(duì)基本類型進(jìn)行比較
- 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);
- // 長(zhǎng)度不相等直接返回false
- if (keysA.length !== keysB.length) {
- return false;
- }
- // key相等的情況下,再去循環(huán)比較
- 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;
- }
函數(shù)組件怎么辦
那么現(xiàn)在問題來了,函數(shù)組件并沒有 shouldComponentUpdate 這個(gè)生命周期,有沒有什么辦法可以避免不必要的 render。
利用高階組件
除了把函數(shù)組件轉(zhuǎn)成類組件,還可以利用高階組件,封裝一個(gè)類似 PureComponet 的功能
- const shouldComponentUpdate = arePropsEqual => BaseComponent => {
- class ShouldComponentUpdate extends React.Component {
- shouldComponentUpdate(nextProps) {
- return arePropsEqual(this.props, nextProps)
- }
- render() {
- return
- }
- }
- ShouldComponentUpdate.displayName = `Pure(${BaseComponent.displayName})`;
- return ShouldComponentUpdate;
- }
- const Pure = BaseComponent => {
- const hoc = shouldComponentUpdate(
- (props, nextProps) => !shallowEqual(props, nextProps)
- )
- return hoc(BaseComponent);
- }
使用 Pure 高階組件的時(shí)候,只需要對(duì)我們的子組件進(jìn)行裝飾即可。
- import React from 'react';
- const Child = (props) =>
{props.name};- export default Pure(Child);
使用 React.memo
React.memo 是 React 16.6 新的一個(gè)API,用來緩存組件的渲染,避免不必要的更新,其實(shí)也是一個(gè)高階組件,與 PureComponent 十分類似,與 PureComponent 不同的是, React.memo 只能用于函數(shù)組件
- 基本用法
- import { memo } from 'react';
- function Button(props) {
- // Component code
- }
- export default memo(Button);
- 高級(jí)用法
默認(rèn)情況下其只會(huì)對(duì) props 做淺層對(duì)比,遇到層級(jí)比較深的復(fù)雜對(duì)象時(shí),表示力不從心了。對(duì)于特定的業(yè)務(wù)場(chǎng)景,可能需要類似 shouldComponentUpdate 這樣的 API,這時(shí)通過 memo 的第二個(gè)參數(shù)來實(shí)現(xiàn)。
- function arePropsEqual(prevProps, nextProps) {
- // your code
- return prevProps === nextProps;
- }
- export default memo(Button, arePropsEqual);
注意:
與 shouldComponentUpdate 不同的是, arePropsEqual 返回 true 時(shí),不會(huì)觸發(fā) render,如果返回 false ,則會(huì)。而 shouldComponentUpdate 剛好與其相反。
合理拆分組件
微服務(wù)的核心思想是:以更輕、更小的粒度來縱向拆分應(yīng)用,各個(gè)小應(yīng)用能夠獨(dú)立選擇技術(shù)、發(fā)展、部署。我們?cè)陂_發(fā)組件的過程中也能用到類似的思想。試想當(dāng)一個(gè)整個(gè)頁面只有一個(gè)組件時(shí),無論哪處改動(dòng)都會(huì)觸發(fā)整個(gè)頁面的重新渲染,去 diffing 和 reconciliation 整個(gè)頁面。在對(duì)組件進(jìn)行拆分之后,render 的粒度更加精細(xì),性能也能得到一定的提升。
總結(jié)
本文主要介紹了如何減少不必要的 render 來提升 React 的性能。在實(shí)際開發(fā)過程中,前端性能問題可能并不常見,隨著業(yè)務(wù)的復(fù)雜度增加,遇到性能問題的概率也會(huì)隨之增加。
- 減少 render 的次數(shù) 類組件可以使用 shouldComponentUpdate 或 PureComponent,函數(shù)組件可以利用高級(jí)組件的特性或者 React.memo
- 對(duì)組件進(jìn)行合理的拆分
在摸索這些解決方案的同時(shí),我們能夠?qū)W習(xí)到諸多經(jīng)典的編程思想,從而更加合理的運(yùn)用框架、技術(shù)解決業(yè)務(wù)問題。
當(dāng)前題目:你的React組件還能跑得再快一點(diǎn)
新聞來源:http://fisionsoft.com.cn/article/djpjhhs.html


咨詢
建站咨詢
