(譯)解密 ES6 語法下 React Class類的記憶體使用

RobottdogCONG發表於2018-02-06

原文連結:Demystifying Memory Usage using ES6 React Classes

作者: Donavon West


在 constructor 中使用 bind, 還是使用箭頭函式作為類屬性方法, 哪種做法更加高效?

Photo by Michal Lomza on Unsplash

現在已經有許多優秀的文章以不同的方式介紹使用 ES6 語法寫類方法。這些文章多數提及了此類方法的表現力(例如執行速度),但我並沒有看到其中有專注於記憶體影響的篇幅。

最近,這個話題在 Axel Rauschmayer 的推動下,被重新提起。對此,許多人表達了他們的觀點與想法,但顯而易見的是,多數人是一知半解的。

I don't like this pattern: class C { handleClick = () => { ... } }

-@rauschma

文章不涉及的內容

這篇文章中,我不會探討執行速度的差異,我不會談論將 lambda 函式傳入元件會打斷 props 的淺比較,我也不會明確的建議你選擇哪兒一種方法來進行編碼。我只會羅列記憶體使用的相關事實,並幫助你做一個周到的決定。

因此,讓我們看一看兩種方案的簡單場景:在 constructor 中使用 bind,或者使用 class 屬性方法。

Constructor bind vs 類屬性方法

我所指的類屬性方法是什麼呢,讓我們看如下的示例:

class MyClass extends Component {
  constructor() {
    super();
    this.state = { clicks: 0 };
  }
  
  handler = () => {
    this.setState(({ clicks }) => ({ clicks: clicks + 1 }));
  }
   
  render() {
    const { clicks } = this.state;
    return(
      <button onClick={this.handler}>
        {`You've clicked me ${clicks} times`}
      </button>
    );
  }
}
複製程式碼

這段示例使用了已經被認可的 ES 類屬性宣告語法,為類的例項新增函式表示式。

同樣,我們使用 constructor bind 方式進行實現。這種語法的實現,在示例中顯得繁瑣,也需要更多時間閱讀程式碼。

class MyClass extends Component {
  constructor() {
    super();
    this.state = { clicks: 0 };
    this.handler = this.handler.bind(this);
  }
  
  handler() {
    this.setState(({ clicks }) => ({ clicks: clicks + 1 }));
  }
  
  render() {
    const { clicks } = this.state;
    return(
      <button onClick={this.handler}>
        {`You've clicked me ${clicks} times`}
      </button>
    );
  }
}
複製程式碼

在我們分析第一個例項中 constructor 方法如何執行前,讓我們確認一下 ES6 的類方法具體做了什麼。回想過去的日子(一兩年前),你是如何在 ES6 類之前編寫這些程式碼的?你可能會這麼寫:

MyClass.prototype.handler = function handler() {
  ...
};
複製程式碼

這就是當你建立類方法時,ES6 語法糖為你做的。那麼,constructor 函式在 ES5 中是如何表現的呢?

function MyClass() {
  this.handler = this.handler.bind(this);
}
複製程式碼

是否與你腦海中想的一樣呢,接下來看一看 MDN 對於 Function.prototype.bind() 方法的說明:

方法建立一個新的函式, 當被呼叫時,將其this關鍵字設定為提供的值,在呼叫新函式時,在任何提供之前提供一個給定的引數序列。

因此,當我們呼叫例項屬性中的 handler(包含一個指向匿名函式的指標),在 constructor 函式中的 bind 方法被呼叫,而 bind 方法繫結了例項的 this 並呼叫原型函式。

就這個例項而言,所花費的記憶體代價很小,僅僅包含指向匿名函式的函式指標,而方法本身處於原型物件上。

兩個方法的行為相同,他們的記憶體足跡又會怎麼變現呢?

記憶體足跡

我繪製瞭如下的圖表幫助我說明各方案的記憶體足跡。紅色區域代表類,綠色區域代表例項,實線框代表記憶體的使用,虛線框表示從類中繼承的方法。就記憶體使用量而言,繼承方法遠小於例項方法。

首先,我們看一下類屬性方法的表現(即使用了箭頭函式的handler

Class properties

注意到基礎的 MyClass 只包含了 render 方法,其他所有的記憶體消耗,來源於每一個例項(實線盒子),每一個例項不僅包含 state、指向 render 的指標,還包含了 handler 方法。當你僅僅建立幾個例項時,或許不是個大問題。

現在,讓我們看一下 bind 方法的表現。

Constructor bind

這裡,基礎的 MyClass 包含了 render 方法以及 handler 方法,這一次,每一個例項只包含 state 以及體積很小、用於呼叫 handler 方法的匿名函式。每一個示例的記憶體足跡要更小。

總的來說,只有當你對同一個類建立大量的例項時,這部分節約的記憶體會表現的很好,例如一個列表項。


總結

當記憶體消耗很少時,使用 constructor bind 方法並不是那麼方便。考慮使用單例的場景,記憶體的結餘或許還不值得編碼的複雜性提升。

在多數情況下,兩種方式沒有過多的差異。考慮到你已經理解了兩者間的差異,在具體場景中正確決策並不是難事。

個人而言,我喜歡類屬性的語法,然而最佳實踐將會是 IMO 推出一個 Babel 將類屬性方法轉換成原型方法。如果你知道這樣的 Babel,或者渴望自己實現,請聯絡我。

請記住:計算機非常擅長閱讀程式碼,你無需擔心。當考慮到讓自己的程式碼可讀性(對人類)更強,使用箭頭函式則更優。


譯者:Robottdog.C

Blog:robottdog.com

相關文章