【譯】快取 React 中事件監聽來提升效能

sundjly發表於2019-04-15

原文連結:Cache your React event listeners to improve performance.
github的地址 歡迎 star!

【譯】快取 React 中事件監聽來提升效能

前言

在 JavaScript 中物件和函式是怎麼被引用好像不被人重視的,但它卻直接影響了 React 的效能。假設你分別創造了兩個完全相同的函式,它們還是不相等的。如下:

const functionOne = function() { alert('Hello world!'); };
const functionTwo = function() { alert('Hello world!'); };
functionOne === functionTwo; // false
複製程式碼

如果你把一個早已經存在的函式賦值給一個變數,比較它們時,你又會發現:

const functionThree = function() { alert('Hello world!'); };
const functionFour = functionThree;
functionThree === functionFour; // true
複製程式碼

物件也是同樣的情況。(記住 JavaScript 中函式即物件)

const object1 = {};
const object2 = {};
const object3 = object1;
object1 === object2; // false
object1 === object3; // true
複製程式碼

如果你其他語言程式設計經驗,你應該熟悉指標的。每次你建立一個物件,計算機都會分配一些記憶體儲存它。當我宣告 object1 = {},會在記憶體分配空間 object1 的變數。object1 又指向了儲存 {} 那塊空間的地址。當我又宣告瞭 object2 = {},又會在記憶體中開闢另一個空間儲存這個新的 {},將 object2 的變數指向了那塊空間的地址。所以 object1object2 指向的地址是不匹配的,這也就是為什麼兩個變數比較不相等的原因。儘管兩個變數指向的地址的內容的鍵-值是一致的,但它們代表的地址指標是不一樣的。

當我進行賦值 object3 = object1,其實我是把 object3object1 指向了記憶體中同一塊空間的地址。 它不是一個新的物件。你可以這樣驗證:


const object1 = { x: true };
const object3 = object1;
object3.x = false;
object1.x; // false
複製程式碼

這個例子中,在記憶體中建立一個物件,object1 指向了那個物件的地址。把 object1 賦值給 object3 的時候,object3 也指向了同一個物件的地址。當改變 object3 的時候,改變了它指向的記憶體空間的物件的鍵-值, 那麼其它所有引用到這個記憶體空間物件的地方都會發生改變。故 object1 也就會發生相同的變化。

對於初級開發者,這是一個常見的錯誤,需要儘量深入的去了解它(本文沒有深入涉及,可以看看《JavaScript高階程式設計》);這篇文章主要是針對 React 效能進行討論的,可能有很多經驗的開發者都沒有考慮過引用型別變數對 React 效能的影響。

你會疑惑變數引用會影響 React 嗎? React 是一個效能很高,減少渲染時間的智慧的庫:如果元件的 state 和 props 沒有改變,那麼 render 的輸出也不會改變。當然,所有的值都相等,根本不需要改變。假設沒有值改變, render 必須返回相同的輸出,因此沒有必要花費時間重新執行。這也是 React 快速的原因,它僅僅在需要的時候才 render。

React 確定元件 props 和 state 的值前後是否相等,用了 JavaScript 中簡單比較 == 的操作符進行的。 React 比較它們是否相等不是對物件進行淺(shallow )比較或者深(deep)比較。淺比較用來描述比較物件的每個鍵值對的術語,通俗點,一般而言是對物件,遍歷它的列舉屬性,依次用Object.is()對物件每個鍵對應的值進行比較,全部相等才判斷為相等。深比較是更進一步,如果這個物件的鍵值對的值是一個物件,則繼續對那個值進行嚴格的相等驗證(繼續用 Object.is()對那個物件的每一個鍵的值判斷),直到沒有物件為止,全部深層次的比較。React 不是如此,它是比較 props 和 state 的引用是否改變。(注意 React 中的 PureComponent 是對 props 和 state 進行的淺比較)。

假如你改變了元件的 props,從{ x: 1}變到另外一個物件 { x: 1}, React 是會重新 render,因為兩個物件在記憶體中的地址不一樣。假如你把元件 props 從 object1(上面例子中)變成 boject3, React 是不會重新 render 的,因為兩個物件是同一個的引用。

在 JavaScript 中,函式也是這種特性(函式即物件)。假如 React 元件 接受了一個功能相同但記憶體地址不同的函式,它也會重新 render。如果 React 接受相同功能的函式引用,它就不會重新 render。

不幸的是,這是我在 code review 中遇到的常見場景:

class SomeComponent extends React.PureComponent {

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={() => alert('!')} />
      </div>
    );
  }
}
複製程式碼

這是一個非常簡單的元件。它是一個按鈕,當點選的時候,它會 alert('!')instructions 屬性告訴你是否應該點選它,這是由 SomeComponent 的 prop 中的 do 來控制的。

每次當 SomeComponent 重新 render (例如 do 從 true 變成 false),Button 元件也會重新 render。onClick 的事件儘管都是一樣的,但每次 render 呼叫都會重新建立。每次 render,一個新的函式在記憶體中儲存,當這個新的記憶體地址的引用傳遞給 Button 元件的時候,Button 元件就會重新渲染,儘管它的輸出沒有什麼改變。

如何修復

如果函式沒有依賴於你的元件(沒有用 this),你可以在元件的外面的定義函式。你所有元件的例項都將會共享相同的一份函式的引用,假定那個函式在所有用例中功能都相同。

const createAlertBox = () => alert('!');

class SomeComponent extends React.PureComponent {

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={createAlertBox} />
      </div>
    );
  }
}
複製程式碼

與先前的例子相反的,每次 render,createAlertBox 都是指向了記憶體中相同的地址,Button 元件絕不會重新 render。

雖然 Button 可能是很小的,渲染很快的元件,(你感受不出來),但是當你在更大的,複雜的元件上看到這些內嵌的函式定義時,你能真實地感受到效能的影響。這是一個非常棒的又簡單的實踐:不要再 render 的方法裡面去定義這些函式。

如果函式依賴於你的元件,你不在元件外部定義它,但你可以把元件的方法作為事件處理函式:

class SomeComponent extends React.PureComponent {

  createAlertBox = () => {
    alert(this.props.message);
  };

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={this.createAlertBox} />
      </div>
    );
  }
}
複製程式碼

當然,每個 SomeComponent元件的例項的彈出框是不同的,這是無法避免的,每一個 SomeComponentButton 元件的點選事件監聽器必須要唯一的(不能互相干擾)。通過呼叫 createAlertBox的方法,你不用關心 SomeComponent是否重新 render,props 的 message 是否改變,Button 元件都不會重新渲染,因為它永遠指向是元件例項的那個方法,這樣能減少不必要渲染,提高你應用的效能。

但是如果我的函式是動態生成的,怎麼處理呢?

(進階)的修復

作者筆記:作者不假思索的寫下下面的例子,來反覆引用記憶體中相同的函式。這些例子旨在讓你更容易地理解引用。作者建議你們閱讀文章這一部分內容來理解引用,更希望你們在評論處給出你自己的理解。一些讀者慷慨地給出了更好的實現,其中考慮到了快取失效和 React 中內建的記憶體管理器。

在單個元件的動態事件處理中,這是一種很常見不唯一的用法,像對一個陣列遍歷:

class SomeComponent extends React.PureComponent {
  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={() => alert(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}
複製程式碼

在這個例子中,你建立了 SomeComponent,宣告瞭動態的數量不固定的 Button,建立了動態的事件監聽器,每個事件監聽函式都是唯一不同的。怎麼解決這個難題呢?

進行記憶,或者更簡單的說法,快取。對於每一個唯一的值,建立並快取函式;對於那個唯一值的所有將來的引用,都返回以前快取的那個函式。

下面展示了我如何實現上面的方法:

class SomeComponent extends React.PureComponent {

  // Each instance of SomeComponent has a cache of click handlers
  // that are unique to it.
  clickHandlers = {};

  // Generate and/or return a click handler,
  // given a unique identifier.
  getClickHandler(key) {

    // If no click handler exists for this unique identifier, create one.
    if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {
      this.clickHandlers[key] = () => alert(key);
    }
    return this.clickHandlers[key];
  }

  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={this.getClickHandler(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}
複製程式碼

list 陣列裡面的每一個目標值都是通過 getClickHandler 的方法呼叫。這個方法在第一次用引數呼叫它時,就會建立一個函式對應那個值,然後返回那個建立的函式。所有將來對那個函式的呼叫都不用再建立新的函式,相反地,它將會返回先前在記憶體中建立的函式的引用。

結果,重新渲染 SomeComponent 將不會導致 Button 的重新渲染。

當它們不只由一個變數決定時,你需要發揮自己的聰明才智,給每一個事件處理生成一個唯一標誌。當然,它並不比簡單地為返回的每個 JSX 物件生成唯一的 key 難多少。

使用索引 index 作為唯一標誌符是需要警告的:如果這個列表 list 改變順序或者刪除某一項你將會得到錯誤的結果。當陣列從 [ 'soda', 'pizza' ] 變為 [ 'pizza' ], 你快取了你的事件監聽器像這樣 listeners[0] = () => alert('soda'),你會發現,當你點選索引是0的 pizza 的 Button時,彈出來是 soda。 這也是 React 建議不要將陣列的索引作為 key 的原因。

最後?

如果你喜歡這篇文章,請點一下贊哦。如果你有任何問題或者更好的建議,請在評論區留言。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!

評論區

  1. 網友一:和 memorize-decorator 庫一起使用是非常棒的

【譯】快取 React 中事件監聽來提升效能

  1. 網友2:這種說法聽過很多次,但效能提升從來沒有被量化。應該需要具體的例子衡量優化前後的影響。

我的觀點

在這個問題不構成效能的主要因素時,可以直接用閉包(或者bind)的方式來解決動態事件監聽問題(可以不做優化);影響效能的時候才進行快取。這篇文章主要是認識一般的 React 元件更新是直接比較 props 和 state 的引用。而 PureComponent 元件則是對 props 和 state 分別前後進行淺比較。這才是我想表達的。

參考

  1. 你真的瞭解淺比較麼?

相關文章