(译)解密 ES6 语法下 React Class类的内存使用

985 阅读4分钟

原文链接: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