React 的内联函数和性能

React 的内联函数和性能

我和妻子近期完成了一次声势浩大的装修。咱们火烧眉毛地想向人们展现咱们的新意。咱们让个人婆婆来参观,她走进那间装修得很漂亮的卧室,抬头看了看那扇构造精巧的窗户,而后说:“竟然没有百叶窗?”😐html

咱们的新卧室;天哪,它看起来就像一张杂志的照片。并且,没有百叶窗。前端

我发现,当我谈论 React 的时候,会有一样的情绪。我将经过研讨会的第一堂课,展现一些很酷的新特性。老是有人说:“内联函数? 我据说它们很慢。”react

并不老是这样,但最近几个月这个观点天天都会出现。做为一名讲师和代码库的做者,这让人感到精疲力竭。不幸的是,我可能有点傻,以前只知道在 Twitter 上咆哮,而不是去写一些可能对别人来讲有深入看法的东西。因此,我就来尝试一下更好的选择了 😂。android

“内联函数”是什么

在 React 的语境中,内联函数是指在 React 进行 "rendering" 时定义的函数。 人们经常对 React 中 "render" 的两种含义感到困惑,一种是指在 update 期间从组件中获取 React 元素(调用组件的 render 方法);另外一种是渲染更新真实的 DOM 结构。本文中提到的 "rendering"都是指第一种。ios

下列是一些内联函数的栗子🌰:git

class App extends Component {
  // ...
  render() {
    return (
      <div>
        
        {/* 1. 一个内联的“DOM组件”事件处理程序 */}
        <button
          onClick={() => {
            this.setState({ clicked: true })
          }}
        >
          Click!
        </button>
        
        {/* 2. 一个“自定义事件”或“操做” */}
        <Sidebar onToggle={(isOpen) => {
          this.setState({ sidebarIsOpen: isOpen })
        }}/>
        
        {/* 3. 一个 render prop 回调 */}
        <Route
          path="/topic/:id"
          render={({ match }) => (
            <div>
              <h1>{match.params.id}</h1>}
            </div>
          )
        />
      </div>
    )
  }
}
复制代码

过早的优化是万恶之源

在开始下一步以前,咱们须要讨论一下如何对程序进行优化。询问任意一个性能方面的专家他们都会告诉你不要过早地优化你的程序。是的,全部具备丰富的性能调优经验的人,都会告诉你不要过早地优化你的代码。github

若是你不去进行测量,你甚至不知道你所作的优化是使得程序变好仍是变得更糟。chrome

我记得个人朋友 Ralph Holzmann 发表的关于 gzip 如何工做的演讲,这个演讲巩固了我对此的见解。他谈到了一个他用古老的脚本加载库 LABjs 作的实验。你能够观看这个视频的 30:02 到 32:35 来了解它,或者继续阅读本文。vim

当时 LABjs 的源码在性能上作了一些使人尴尬的事情。它没有使用普通的对象表示法(obj.foo),而是将键存储在字符串中,并使用方括号表示法来访问对象(obj[stringForFoo])。这样作的想法源于,通过小型化和 gzip 压缩以后,非天然编写的代码将比天然编写的代码体积小。你能够在这里看到它后端

Ralph fork 了源代码,没有去考虑如何优化以实现小型化 和 gzip,而是经过天然地编写代码移除了优化的部分。

事实证实,移除“优化部分”后,文件大小削减了 5.3%!若是你不去进行测量,你甚至不知道你所作的优化是使得程序变好仍是变得更糟!

过早的优化不只会占用开发时间,损害代码的整洁,甚至会产生拔苗助长的结果致使性能问题,就像 LABjs 那样。若是做者一直在进行测量,而不只仅是想象性能问题,就会节省开发时间,同时能让代码更简洁,性能更好。

不要过早地进行优化。好了,回到 React 。

为何人们说内联函数很慢?

两个缘由:内存/垃圾回收问题和 shouldComponentUpdate

内存和垃圾回收

首先,人们(和 eslint configs)担忧建立内联函数产生的内存和垃圾回收成本。在箭头函数普及以前,不少代码都会内联地调用 bind ,这在历史上表现不佳。例如:

<div>
  {stuff.map(function(thing) {
    <div>{thing.whatever}</div>
  }.bind(this)}
</div>
复制代码

Function.prototype.bind 的性能问题在此获得了解决,并且箭头函数要么是原生函数,要么是由 Babel 转换为普通函数;在这两种状况下,咱们均可以假定它并不慢。

记住,你不要坐在那里而后想象“我赌这个代码确定慢”。你应该天然地编写代码,而后测量它。若是存在性能问题,就修复它们。咱们不须要证实一个内联的箭头函数是快的,也不须要另外一些人来证实它是慢的。不然,这就是一个过早的优化。

据我所知,尚未人对他们的应用程序进行分析,代表内联箭头函数很慢。在进行分析以前,这甚至不值得谈论 —— 但不管如何,我会提供一个新思路 😝

若是建立内联函数的成本很高,以致于须要使用 eslint 规则来规避它,那么咱们为何要将该开销转移到初始化的热路径上呢?

class Dashboard extends Component {
  state = { handlingThings: false }
  
  constructor(props) {
    super(props)
    
    this.handleThings = () =>
      this.setState({ handlingThings: true })

    this.handleStuff = () => { /* ... */ }

    // bind 的开销更昂贵
    this.handleMoreStuff = this.handleMoreStuff.bind(this)
  }

  handleMoreStuff() { /* ... */ }

  render() {
    return (
      <div>
        {this.state.handlingThings ? (
          <div>
            <button onClick={this.handleStuff}/>
            <button onClick={this.handleMoreStuff}/>
          </div>
        ) : (
          <button onClick={this.handleThings}/>
        )}
      </div>
    )
  }
}
复制代码

由于过早地优化,咱们已经将组件的初始化速度下降了 3 倍!若是全部处理程序都是内联的,那么在初始化中只须要建立一个函数。相反的,咱们则要建立 3 个。咱们没有测量任何东西,因此没有理由认为这是一个问题。

若是你想彻底忽略这一点,那么就去制定一个 eslint 规则,来要求在任何地方都使用内联函数来加快初始渲染速度🤦🏾‍♀。

PureComponent 和 shouldComponentUpdate

这才是问题真正的症结所在。你能够经过理解两件事来看到真正的性能提高: shouldComponentUpdate 和 JavaScript 严格相等的比较。若是不能很好地理解它们,就可能在无心中以性能优化的名义使 React 代码更难处理。

当你调用 setState 时,React 会将旧的 React 元素与一组新的 React 元素进行比较(这称为 r_econciliation_ ,你能够在这里阅读相关资料 ),而后使用该信息更新真实的 DOM 元素。有时候,若是你有不少元素须要检查,这个过程就会变得很慢(好比一个大的 SVG )。React 为这类状况提供了逃生舱口,名叫 shouldComponentUpdate

class Avatar extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return stuffChanged(this, nextProps, nextState))
  }
  
  render() {
    return //...
  }
}
复制代码

若是你的组件定义了 shouldComponentUpdate ,那么在 React 进行新旧元素对比以前,它会询问 shouldComponentUpdate 有没有变动发生。若是返回了false,那么React将会直接跳过元素diff检查,从而节省一些时间。若是你的组件足够大,这会对性能产生至关大的影响。

优化组件的最多见方法是扩展 "React.PureComponent" 而不是 "React.Component" 。一个 PureComponent 会在 shouldComponentUpdate 中比较 props 和 state ,这样你就不用手动执行了。

class Avatar extends React.PureComponent { ... }
复制代码

当被要求更新时,Avatar 会对它的 props 和 state 使用一个严格相等比较,但愿以此来加快速度。

严格相等比较

JavaScript 中有六种基本类型:string, number, boolean, null, undefined, 和 symbol。当你对两个值相同的基本类型进行“严格相等比较”的时候,你会获得一个 true 值。举个例子🌰:

const one = 1
const uno = 1
one === uno // true
复制代码

PureComponent 比较 props 时,它会使用严格相等比较。这对内联原始值很是有效: <Toggler isOpen={true}/>

prop 的比较只会在有非原始类型们出现的时候产生问题——啊,说错了,抱歉,是类型而不是类型们。只有一种其余类型,那就是 Object。你问函数和数组?事实上,它们都是对象(Object)。

函数是具备附加的可调用功能的常规对象。

哈哈哈,不愧是 JavaScript。不管如何,对对象使用严格相等检查,即便表面上看起来相等的值,也会被断定为 false(不相等):

const one = { n: 1 }
const uno = { n: 1 }
one === uno // false
one === one // true
复制代码

因此,若是你在 JSX 中内联地使用一个对象,它会使 PureComponent 的 prop diff 检查失效,转而使用较昂贵的方式对 React 元素进行 diff 检查。元素的 diff 将变为空,这样就浪费了两次进行差别比较的时间。

// 第一次 render
<Avatar user={{ id: 'ryan' }}/>

// 下一次 render
<Avatar user={{ id: 'ryan' }}/>

// prop diff 认为有东西发生了变化,由于 {} !== {}
// 元素 diff 检查 (reconciler) 发现没有任何变化
复制代码

因为函数是对象,并且 PureComponent 会对 props 进行严格相等的检查,所以,一个内联的函数将老是没法经过 prop 的 diff 检查,从而转向 reconciler 中的元素 diff 检查。

能够看出,这不只仅只关乎内联函数。函数简直就是 object, function, array 三部曲演绎推广的主唱。

为了让 shouldComponentUpdate 高兴,你必须保持函数的引用标识。对经验丰富的 JavaScript 开发者来讲,这不算糟。可是 Michael 和我领导了一个有3500多人参加的研讨会,他们的开发经验各不相同,而这对不少人来讲都并不容易。ES 的类也没有提供引导咱们进入各类 JavaScript 路径的帮助:

class Dashboard extends Component {
  constructor(props) {
    super(props)
    
    // 使用 bind ?拖慢初始化的速度,看上去不妙
    // 当你有 20 个 bind 的时候(我见过你的代码,我知道)
    // 它会增长打包后文件的大小
    this.handleStuff = this.handleStuff.bind(this)

    // _this 一点也不优雅
    var _this = this
    this.handleStuff = function() {
      _this.setState({})
    }
    
    // 若是你会用 ES 的类,那你极可能会使用箭头
    // 函数(经过 babel ,或使用现代浏览器)。这不是很难可是
    // 把你全部的处理程序都放在构造函数中就
    // 不太好了
    this.handleStuff = () => {
      this.setState({})
    }
  }
  
  // 这个很不错,但它不是 JavaScript ,至少如今还不是,因此如今
  // 咱们要讨论的是 TC39 如何工做,并评估咱们的草案
  // 阶段风险容忍度
  handleStuff = () => {}
}
复制代码

学习如何保持函数的引用标识将会引出一个使人惊讶的长篇大论。

一般没有理由强迫人们这么作,除非有一个 eslint 配置对他们大喊大叫。我想展现的是,内联函数和提高性能二者能够兼得。但首先,我想讲一个我本身遇到的性能相关的故事。

我使用 PureComponent 的经历

当我第一次了解到 PureRenderMixin(在 React 的早期版本中叫这个,后来改成 PureComponent )时,我进行了大量的测试,来测试个人应用程序的性能。而后,我将 PureRenderMixin 添加到每一个组件中。当我采起了一套优化后的测量方法时,我但愿有一个关于一切变得有多快的很酷的故事能够讲。

让人大跌眼镜的是,个人应用程序变慢了 🤔。

为何呢?仔细想一想,若是你有一个 Component ,会有多少次 diff 检查?若是你有一个 PureComponent ,又会有多少次 diff 检查?答案分别是“只有一次”和“至少一次,有时是两次”。若是一个组件常常在更新时发生变化,那么 PureComponent 将会执行两次 diff 检查而不是一次(props 和 state 在 shouldComponentUpdate 中进行的严格相等比较,以及常规的元素 diff 检查)。这意味着一般它会变慢,偶尔会变快。显然,个人大部分组件大部分时间都在变化,因此总的来讲,个人应用程序变慢了。啊哦😯。

在性能方面没有银弹。你必须测量。

三种情景

在本文的开头,我展现了三种内联函数。如今咱们已经了解了一些背景,让咱们来一一讨论一下它们。可是请记住,在你有一个衡量标准来断定以前,请先将 PureComponent 束之高阁。

DOM 组件事件处理程序

<button
  onClick={() => this.setState(…)}
>click</button>
复制代码

一般,在 buttons,inputs,和其余 DOM 组件的事件处理程序中,除了 setState 之外,不会作其余的事情。这让内联函数成为了一般状况下最干净的方法。它们不是在文件中跳来跳去寻找事件处理程序,而是把内容放在同一位置。React 社区一般欢迎这种方式。

button 组件(以及全部其余的DOM组件)甚至都算不上是 PureComponent,因此这里也不存在 shouldComponentUpdate 引用标识的问题。

因此,认为这个过程很慢的惟一缘由是,你是否定为简单地定义一个函数会产生足以让人担忧的开销。咱们已经讨论过,这在任何地方都未被证明。这只是纸上谈兵的性能假设。在被证明以前,这样作没问题。

一个“自定义事件”或“操做”

<Sidebar onToggle={(isOpen) => {
  this.setState({ sidebarIsOpen: isOpen })
}}/>
复制代码

若是 SidebarPureComponent,咱们将会打破 prop 的 diff 检查。再一次,因为处理程序很简单,最好把它们都放在同一位置。

对于像 onToggle 这样的事件,Sidebar 还有什么必要对它执行 diff 检查呢?只有两种状况才须要将 prop 包含在 shouldComponentUpdate 的 diff 检查中:

  1. 你使用 prop 来进行渲染
  2. 你使用 prop 来在 componentWillReceivePropscomponentDidUpdate,或者 componentWillUpdate 中产生一些其余的做用

大多数 on<whatever> prop 都不符合这些要求。所以,多数 PureComponent 的用法都会致使屡次执行 diff 检查,迫使开发人员没必要要地维护处理程序的引用标识。

咱们只应该对会产生影响的 prop 执行 diff 检查。这样,人们就能够将处理程序放在同一位置,而且仍然能够得到想要寻求的性能提高(并且因为咱们关心性能,因此咱们但愿执行更少次数的 diff 检查!)

对于大多数组件,我建议建立一个 PureComponentMinusHandlers 类并从中继承,而不是从 PureComponent 中继承。它能够跳过对函数的全部检查。鱼与熊掌兼得。

好吧,差很少是这样的。

若是你接收到一个函数并直接将它传递给另外一个组件,它将会没法及时更新。看一下这个:

// 1. App 会传递一个 prop 给 From 表单
// 2. Form 将向下传递一个函数给 button
//    这个函数与它从 App 获得的 prop 相接近
// 3. App 会在 mounting 以后 setState,并传递
//    一个**新**的 prop 给 Form
// 4. Form 传递一个新的函数给 Button,这个函数与
//    新的 prop 相接近
// 5. Button 会忽略新的函数, 并没有法
//    更新点击处理程序,从而提交陈旧的数据

class App extends React.Component {
  state = { val: "one" }

  componentDidMount() {
    this.setState({ val: "two" })
  }

  render() {
    return <Form value={this.state.val} />
  }
}

const Form = props => (
  <Button
    onClick={() => {
      submit(props.value)
    }}
  />
)

class Button extends React.Component {
  shouldComponentUpdate() {
    // 让咱们伪装比较了除函数之外的一切东西
    return false
  }

  handleClick = () => this.props.onClick()

  render() {
    return (
      <div>
        <button onClick={this.props.onClick}>这个的数据是旧的</button>
        <button onClick={() => this.props.onClick()}>这个工做正常</button>
        <button onClick={this.handleClick}>这个也工做正常</button>
      </div>
    )
  }
}
复制代码

这是一个运行该应用程序的沙箱

所以,若是你喜欢从 PureRenderWithoutHandlers 继承的想法,请确保永远不要将你要在 diff 检查中要忽略的处理程序直接传递给其余组件——你须要以某种方式包装它们。

如今,咱们要么必须维护引用标识,要么必须避免引用标识!欢迎来到性能优化。至少在这种方法中,必须处理的是优化组件,而不是使用它的代码。

我要坦率地说,这个示例应用程序是我在发布 Andrew Clark 后所作的编辑,它引发了个人注意。在这里,您认为我足够聪明,知道何时管理引用标识,何时无论理了吧!😂

一个 render prop

<Route
  path="/topic/:id"
  render={({ match }) => (
    <div>
      <h1>{match.params.id}</h1>}
    </div>
  )
/>
复制代码

用来渲染的 prop 是一种模式,它用来建立一个用于组成和管理共享状态的组件。(你能够在这里了解更多)。它的内容对组件来讲是未知的,举个栗子🌰:

const App = (props) => (
  <div>
    <h1>Welcome, {props.name}</h1>
    <Route path="/" render={() => (
      <div>
        {/*
          prop.name 是从路由外部传入的,它不是做为 prop 传递进来的,
          所以路由不能可靠地成为一个PureComponent,它
          不知道在组件内部会渲染什么
        */}
        <h1>Hey, {props.name}, let's get started!</h1> </div> )}/> </div> ) 复制代码

这意味着一个内联的用来渲染的 prop 函数不会致使 shouldComponentUpdate 的问题:它永远没有足够的信息来成为一个 PureComponent

因此,惟一的反对意见又回到了相信简单地定义一个函数是缓慢的。重复第一个例子:没有证据支持这一观点。这只是纸上谈兵的性能假设。

总结

  1. 天然地编写代码,设计代码
  2. 测量你的交互,找到慢在哪里。这里是方法.
  3. 仅在须要的时候使用 PureComponentshouldComponentUpdate,避免使用 prop 函数(除非它们在生命周期的钩子函数中为产生某种做用而使用)。

若是你真的相信过早的优化不是好主意,那么你就不须要证实内联函数是快的,而是须要证实它们是慢的。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索