【译】缓存 React 中事件监听来提高性能

原文连接:Cache your React event listeners to improve performance.
github 的地址 欢迎 star!react

前言

在 JavaScript 中对象和函数是怎么被引用好像不被人重视的,但它却直接影响了 React 的性能。假设你分别创造了两个彻底相同的函数,它们仍是不相等的。以下:git

const functionOne = function() { alert('Hello world!'); };
const functionTwo = function() { alert('Hello world!'); };
functionOne === functionTwo; // false
复制代码

若是你把一个早已经存在的函数赋值给一个变量,比较它们时,你又会发现:github

const functionThree = function() { alert('Hello world!'); };
const functionFour = functionThree;
functionThree === functionFour; // true
复制代码

对象也是一样的状况。(记住 JavaScript 中函数即对象)web

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 性能的影响。bash

你会疑惑变量引用会影响 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 进行的浅比较)。ide

假如你改变了组件的 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 库一块儿使用是很是棒的

  1. 网友2:这种说法听过不少次,但性能提高历来没有被量化。应该须要具体的例子衡量优化先后的影响。

个人观点

在这个问题不构成性能的主要因素时,能够直接用闭包(或者bind)的方式来解决动态事件监听问题(能够不作优化);影响性能的时候才进行缓存。这篇文章主要是认识通常的 React 组件更新是直接比较 props 和 state 的引用。而 PureComponent 组件则是对 props 和 state 分别先后进行浅比较。这才是我想表达的。

参考

  1. 你真的了解浅比较么?
相关文章
相关标签/搜索