PureComponent 使用注意事項以及源碼解析

本文簡要介紹了 React 中 PureComponent 與 Component 的區別以及使用時須要注意的問題,並在後面附上了源碼解析,但願對有疑惑的朋友提供一些幫助。css

前言

先介紹一下 PureComponent,平時咱們建立 React 組件通常是繼承於 Component,而 PureComponent 至關因而一個更純淨的 Component,對更新先後的數據進行了一次淺比較。只有在數據真正發生改變時,纔會對組件從新進行 render。所以能夠大大提升組件的性能。react


對比 Component 和 PureComponent

繼承 Component 建立組件

App.js

裏面的 state 有兩個屬性,text 屬性是基本數據類型,todo 屬性是引用類型。針對這兩種數據類型分別進行對比:git

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 rendergithub

  • 點擊 5 次 ·更改文字· 按鈕,能夠看到控制檯再次多打印了 5 次 log,瀏覽器中的 Hello 文字變成了 World瀏覽器

  • 點擊 5 次 ·更改計劃· 按鈕,控制檯同樣多打印 5 次 log,瀏覽器中的 學習 React 計劃變成了 學習 Vue函數

分析一下,其實 5 次點擊中只有一次是有效的,後來的數據其實並無真正改變,可是因爲依然使用了 setState(),因此仍是會從新 render。因此這種模式是比較消耗性能的。工具

繼承 PureComponent

其實 PureComponent 用法也是和 Component 同樣,只不過是將繼承 Component 換成了 PureComponent。oop

App.js

...
// 上面的代碼和以前一致

class App extends PureComponent {
    // 下面的代碼也和以前同樣
    ...
}

export default App;
複製代碼

瀏覽器中界面

界面顯示

測試

和上面 Component 的測試方式同樣源碼分析

  • 點擊 5 次 ·更改文字· 按鈕,能夠看到控制檯只多打印了** 1 次** log,瀏覽器中的 Hello 文字變成了 Worldpost

  • 點擊 5 次 ·更改計劃· 按鈕,控制檯只多打印了 1 次 log,瀏覽器中的 學習 React 計劃變成了 學習 Vue

由此能夠看出,使用 PureComponent 仍是比較節省性能的,即使是使用了 setState(),也會在數據真正改變時纔會從新渲染組件

使用時可能遇到的問題

下面咱們將代碼中 changeTextchangeTodo 方法修改一下

/** * 修改 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 源碼分析總結

相關文章
相關標籤/搜索