优化React重现器的一个简单技巧(附代码)

69 阅读6分钟

我在准备一篇与React重现有关的博文时,偶然发现了这个React的小知识宝库,我想你会非常欣赏。

读完这篇博文后,Brooks Lybrand实施了这个技巧,结果是这样的。

兴奋吗?让我们用一个简单的假想的例子来分解它,然后谈谈这对你的日常应用有什么实际应用。

一个例子

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-g9mt5

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      <Logger label="counter" />
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'))

当运行时,"counter rendered "最初会被记录到控制台,每次增加计数时,"counter rendered "会被记录到控制台。这是因为当按钮被点击时,状态发生了变化,React需要根据状态的变化获得新的React元素来渲染。 当它获得这些新元素时,它渲染并提交到DOM中。

这里是事情变得有趣的地方。考虑到这样一个事实:<Logger label="counter" /> ,在渲染之间从不改变。它是静态的,因此可以被提取出来。让我们尝试一下,只是为了好玩(我并不推荐你这样做,等待博文后面的实际建议)。

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

你注意到这个变化了吗?是的!我注意到了。我们得到了最初的日志,但后来当我们点击按钮时就不再有新的日志了!WHAAAAT!?

如果你想跳过所有的深层技术细节,进入 "这对我意味着什么",请继续,现在就把自己放在那里

发生了什么事?

那么是什么造成了这种差异呢?嗯,这与React元素有关。你为什么不花点时间阅读我的博文"什么是JSX?"来快速复习一下React元素以及它们与JSX的关系。

当React调用counter函数时,它得到的东西看起来有点像这样。

// some things removed for clarity
const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment, // this is the click handler function
          children: 'The count is 0',
        },
      },
      {
        type: Logger, // this is our logger component function
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

这些被称为UI描述器对象。它们描述了React应该在DOM中创建的用户界面(或者通过react native的本地组件)。让我们点击按钮,看一下变化。

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

button 就我们所知,唯一的变化是onClickchildren 这个元素的prop。然而,整个东西都是全新的!自从使用React以来,你在每次渲染时都会全新地创建这些对象。(幸运的是,即使是移动浏览器在这方面也是相当快的,所以这从来没有成为一个重要的性能问题)。

实际上,研究React元素树的哪些部分在不同的渲染中是相同的,可能会更容易一些,所以下面是这两次渲染中没有变化的部分。

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

所有的元素类型都是一样的(这很典型),Logger元素的label 道具也没有变化。然而,道具对象本身在每次渲染时都会发生变化,尽管该对象的属性与之前的道具对象是一样的。

**好吧,这里有一个关键问题。**因为Logger的props对象发生了变化,React需要重新运行Logger函数,以确保它不会根据新的props对象得到任何新的JSX(除了可能需要根据props变化运行的效果)。**但如果我们能防止道具在渲染之间发生变化呢?**如果道具不改变,那么React就知道我们的效果不需要重新运行,我们的JSX也不应该改变(因为React依赖于我们的渲染方法应该是empotent的)。 这正是React在这里的编码,从React开始就一直是这样做的

好吧,但问题是,每次我们创建React元素时,react都会创建一个全新的props 对象,那么我们如何确保props对象在渲染之间不发生变化呢? 希望现在你已经明白了,你也明白了为什么上面的第二个例子没有重新渲染Logger了。如果我们创建一次JSX元素并重新使用同一个元素,那么我们每次都会得到相同的JSX!

让我们把它重新组合起来

这里又是第二个例子(所以你不必再往上滚动)

// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f

import * as React from 'react'
import ReactDOM from 'react-dom'

function Logger(props) {
  console.log(`${props.label} rendered`)
  return null // what is returned here is irrelevant...
}

function Counter(props) {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <button onClick={increment}>The count is {count}</button>
      {props.logger}
    </div>
  )
}

ReactDOM.render(
  <Counter logger={<Logger label="counter" />} />,
  document.getElementById('root'),
)

所以,让我们来看看在不同渲染之间有哪些东西是相同的

const counterElement = {
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: increment,
          children: 'The count is 1',
        },
      },
      {
        type: Logger,
        props: {
          label: 'counter',
        },
      },
    ],
  },
}

因为记录器元素是完全不变的(因此道具也是不变的),React可以自动为我们提供这种优化,而不必费力地重新渲染记录器元素,因为无论如何它都不需要重新渲染了。这基本上就像React.memo ,只不过React不是单独检查每个props,而是整体检查props对象。

那么这对我来说意味着什么呢?

总之,如果你遇到了性能问题,可以试试这个:

  1. 将昂贵的组件 "提升 "到一个父级,在那里它被渲染的频率会更低。
  2. 然后将昂贵的组件作为一个道具传递下去。

你可能会发现这样做可以解决你的性能问题,而不需要像一个巨大的侵入性创可贴一样在你的代码库中到处传播React.memo 🤕😉

演示

在React中创建一个实用的慢速应用程序的演示是很棘手的,因为它有点需要建立一个完整的应用程序,但我确实有一个精心设计的应用程序的例子,有一个之前/之后,你可以检查和玩。

我想补充的一点是,尽管使用这段代码的快速版本更好,但它在最初渲染时的表现仍然非常糟糕,如果它真的需要再做一次自上而下的重新渲染(或者当你更新行/列时),它的表现也会非常糟糕。这是一个性能问题,也许应该根据其本身的优点来处理(不管重新渲染的必要性如何)。另外,请记住,codesandbox使用的是React的开发版本,它给你带来了非常好的开发体验,但性能比React的生产版本慢得多。

这也不仅仅是在你的应用程序的顶层有用的东西,这可以应用到你的应用程序的任何地方,它是有意义的。我喜欢这一点的原因是:"它既是自然的构成*,又*是一个优化的机会。"(这是丹的说法)。所以我自然而然地做这件事,并免费获得香水的好处。这也是我一直喜欢React的原因。React是这样写的:默认情况下,习惯性的React应用程序是快速的,然后React提供了优化助手,供你作为逃生舱口使用。

祝您好运!