react状态提升_React:提升状态将杀死您的应用程序

react状态提升

Cover

Have you heard about "lifting state up"? I guess you have and that's the exact reason why you're here. How could it be possible that one of the 12 main concepts listed in React official documentation might lead to poor performance? Within this article, we'll consider a situation when it's indeed the case.

您听说过“提起国家”吗? 我猜你有,这就是你在这里的确切原因。 React官方文档中列出的12个主要概念之一可能会导致性能下降吗? 在本文中,我们将考虑实际情况。

步骤1:提起 (Step 1: Lift it up)

I suggest you to create a simple game of tic-tac-toe. For the game we'll need:

我建议您创建一个简单的井字游戏。 对于游戏,我们需要:

  • Some game state. No real game logic to find out if we win or lose. Just a simple two-dimensional array filled with either undefined, "x" or "0".

    一些游戏状态。 没有真正的游戏逻辑来找出我们是输是赢。 只是一个简单的二维数组,其中填充了undefined"x""0".

    const size = 10
    // Two-dimensional array (size * size) filled with `undefined`. Represents an empty field.
    const initialField = new Array(size).fill(new Array(size).fill(undefined))
  • A parent container to host our game's state.

    一个父容器来托管我们的游​​戏状态。

    const App = () => {
     const [field, setField] = useState(initialField)
    
     return (
       <div>
         {field.map((row, rowI) => (
           <div>
             {row.map((cell, cellI) => (
               <Cell
                 content={cell}
                 setContent={
                   // Update a single cell of a two-dimensional array
                   // and return a new two dimensional array
                   (newContent) =>
                     setField([
                       // Copy rows before our target row
                       ...field.slice(0, rowI),
                       [
                         // Copy cells before our target cell
                         ...field[rowI].slice(0, cellI),
                         newContent,
                         // Copy cells after our target cell
                         ...field[rowI].slice(cellI + 1),
                       ],
                       // Copy rows after our target row
                       ...field.slice(rowI + 1),
                     ])
                 }
               />
             ))}
           </div>
         ))}
       </div>
     )
    }
  • A child component to display a state of a single cell.

    显示单个单元格状态的子组件。

    const randomContent = () => (Math.random() > 0.5 ? 'x' : '0')
    
    const Cell = ({ content, setContent }) => (
     <div onClick={() => setContent(randomContent())}>{content}</div>
    )

Live demo #1

现场演示#1

So far it looks well. A perfectly reactive field that you can interact with at the speed of light :) Let's increase the size. Say, to 100. Yeah, it's time to click on that demo link and change size variable on the very top. Still fast for you? Try 200 or use CPU throttling built into Chrome. Do you see now a significant lag between the time you click on a cell and the time its content changes?

到目前为止看起来还不错。 您可以以光速交互的完美React场:)让我们增大尺寸。 假设是100。是的,是时候单击该演示链接并在最顶部更改size变量了。 还是为您快速? 尝试200或使用Chrome内置的CPU节流 。 您现在看到单击单元格的时间和其内容更改的时间之间有很大的滞后吗?

Let's change size back to 10 and add some profiling to investigate the cause.

让我们将size改回10,并添加一些配置文件以调查原因。

const Cell = ({ content, setContent }) => {
  console.log('cell rendered')
  return <div onClick={() => setContent(randomContent())}>{content}</div>
}

Live demo #2

现场演示#2

Yep, that's it. Simple console.log would suffice as it runs on every render.

是的,就是这样。 只要在每个渲染器上运行,简单的console.log就足够了。

So what do we see? Based on the number on "cell rendered" statements (for size = N it should be N) in our console it seems like the entire field is re-rendered each time a single cell changes.

那我们看到了什么? 根据控制台中“单元格渲染”语句的数量(对于size = N,应为N),似乎每次单个单元格更改时都会重新渲染整个字段。

The most obvious thing to do is to add some keys as React documentation suggests.

最明显的事情是按照React文档的建议添加一些密钥。

<div>
  {field.map((row, rowI) => (
    <div key={rowI}>
      {row.map((cell, cellI) => (
        <Cell
          key={`row${rowI}cell${cellI}`}
          content={cell}
          setContent={(newContent) =>
            setField([
              ...field.slice(0, rowI),
              [
                ...field[rowI].slice(0, cellI),
                newContent,
                ...field[rowI].slice(cellI + 1),
              ],
              ...field.slice(rowI + 1),
            ])
          }
        />
      ))}
    </div>
  ))}
</div>

Live demo #3

现场演示#3

However, after increasing size again we see that that problem is still there. If only we could see why any component renders… Luckily, we can with some help from amazing React DevTools. It's capable of recording why components get rendered. You have to manually enable it though.

但是,再次增大size后,我们发现该问题仍然存在。 只要能看到为什么渲染任何组件…幸运的是,我们可以在惊人的React DevTools的帮助下获得帮助。 它能够记录为什么渲染组件。 但是,您必须手动启用它。

React DevTools settings

Once it's enabled, we can see that all cells were re-rendered because their props changed, specifically, setContent prop.

启用后,我们可以看到所有单元均被重新渲染,因为它们的道具已更改,特别是setContent道具。

React DevTools report #1

Each cell has two props: content and setContent. If cell [0][0] changes, content of cell [0][1] doesn't change. On the other hand, setContent captures field, cellI and rowI in its closure. cellI and rowI stay the same, but field changes with every change of any cell.

每个单元格都有两个道具: contentsetContent 。 如果单元格[0] [0]更改,则单元格[0] [1]的内容不变。 另一方面, setContent在其闭包中捕获fieldcellIrowIcellIrowI保持不变,但是field随任何单元格的每次更改而改变。

Let's refactor our code and keep setContent the same.

让我们重构代码并保持setContent不变。

To keep the reference to setContent the same we should get rid of the closures. We could eliminate cellI and rowI closure by making our Cell explicitly pass cellI and rowI to setContent. As to field, we could utilize a neat feature of setStateit accepts callbacks.

为了使对setContent的引用相同,我们应该删除闭包。 我们可以通过使Cell显式地将cellIrowI传递给setContent来消除cellIrowI闭包。 至于field ,我们可以利用setState的简洁功能- 它接受回调

const [field, setField] = useState(initialField)

// `useCallback` keeps reference to `setCell` the same.
const setCell = useCallback(
  (rowI, cellI, newContent) =>
    setField((oldField) => [
      ...oldField.slice(0, rowI),
      [
        ...oldField[rowI].slice(0, cellI),
        newContent,
        ...oldField[rowI].slice(cellI + 1),
      ],
      ...oldField.slice(rowI + 1),
    ]),
  [],
)

Which makes App look like this

这使得App看起来像这样

<div>
  {field.map((row, rowI) => (
    <div key={rowI}>
      {row.map((cell, cellI) => (
        <Cell
          key={`row${rowI}cell${cellI}`}
          content={cell}
          rowI={rowI}
          cellI={cellI}
          setContent={setCell}
        />
      ))}
    </div>
  ))}
</div>

Now Cell has to pass cellI and rowI to the setContent.

现在, Cell必须将cellIrowI传递给setContent

const Cell = ({ content, rowI, cellI, setContent }) => {
  console.log('cell render')
  return (
    <div onClick={() => setContent(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
}

Live demo #4

现场演示#4

Let's take a look at the DevTools report.

让我们看一下DevTools报告。

React DevTools report #2

What?! Why the heck does it say "parent props changed"? So the thing is that every time our field is updated Appis re-rendered. Therefore its child components are re-rendered. Ok. Does stackoverflow say anything useful about React performance optimization? Internet suggests to use shouldComponentUpdate or its close relatives: PureComponent and memo.

什么?! 为什么说“父母的道具变了”? 所以事情是,每次我们的领域更新时, App都会重新渲染。 因此,其子组件将被重新渲染。 好。 stackoverflow是否说了一些有关React性能优化的有用信息? Internet建议使用shouldComponentUpdate或其近亲: PureComponentmemo

const Cell = memo(({ content, rowI, cellI, setContent }) => {
  console.log('cell render')
  return (
    <div onClick={() => setContent(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
})

Live demo #5

现场演示#5

Yay! Now only one cell is re-rendered once its content changes. But wait… Was there any surprise? We followed best practices and got the expected result.

好极了! 现在,一旦其内容更改,仅一个单元会被重新渲染。 但是等等...有什么惊喜吗? 我们遵循最佳实践,并获得了预期的结果。

An evil laugh was supposed to be here. As I'm not with you, please, try as hard as possible to imagine it. Go ahead and increase size in Live demo #5. This time you might have to go with a little bigger number. However, the lag is still there. Why???

本来应该有一个邪恶的笑声。 由于我不在您身边,请尽力想象。 继续并在Live demo#5中增加size 。 这次,您可能需要使用更大的数字。 但是,滞后仍然存在。 为什么???

Let's take a look at the DebTools report again.

让我们再次查看DebTools报告。

React DevTools report #3

There's only one render of Cell and it was pretty fast, but there's also a render of App, which took quite some time. The thing is that with every re-render of App each Cell has to compare its new props with its previous props. Even if it decides not to render (which is precisely our case), that comparison still takes time. O(1), but that O(1) occurs size * size times!

只有一个Cell渲染,而且速度非常快,但是还有一个App渲染,这花费了很多时间。 事实是,每次重新渲染App每个Cell都必须将其新道具与以前的道具进行比较。 即使决定不渲染(这正是我们的情况),该比较仍然需要时间。 O(1),而O(1)发生size * size倍!

步骤2:将其向下移动 (Step 2: Move it down)

What can we do to work around it? If rendering App costs us too much, we have to stop rendering App. It's not possible if keep hosting our state in App using useState, because that's exactly what triggers re-renders. So we have to move our state down and let each Cell subscribe to the state on its own.

我们可以做些什么来解决呢? 如果渲染App花费太多,我们必须停止渲染App 。 如果继续使用useStateApp托管我们的州是useState ,因为这正是触发重新渲染的原因。 因此,我们必须向下移动状态,并让每个Cell自己订阅该状态。

Let's create a dedicated class that will be a container for our state.

让我们创建一个专用的类,该类将成为我们状态的容器。

class Field {
  constructor(fieldSize) {
    this.size = fieldSize
    // Copy-paste from `initialState`
    this.data = new Array(this.size).fill(new Array(this.size).fill(undefined))
  }

  cellContent(rowI, cellI) {
    return this.data[rowI][cellI]
  }

  // Copy-paste from  old `setCell`
  setCell(rowI, cellI, newContent) {
    console.log('setCell')
    this.data = [
      ...this.data.slice(0, rowI),
      [
        ...this.data[rowI].slice(0, cellI),
        newContent,
        ...this.data[rowI].slice(cellI + 1),
      ],
      ...this.data.slice(rowI + 1),
    ]
  }

  map(cb) {
    return this.data.map(cb)
  }
}

const field = new Field(size)

Then our App could look like this:

然后我们的App可能看起来像这样:

const App = () => {
  return (
    <div>
      {// As you can see we still need to iterate over our state to get indexes.
      field.map((row, rowI) => (
        <div key={rowI}>
          {row.map((cell, cellI) => (
            <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} />
          ))}
        </div>
      ))}
    </div>
  )
}

And our Cell can display the content from field on its own:

并且我们的Cell可以自行显示field的内容:

const Cell = ({ rowI, cellI }) => {
  console.log('cell render')
  const content = field.cellContent(rowI, cellI)
  return (
    <div onClick={() => field.setCell(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
}

Live demo #6

现场演示#6

At this point, we can see our field being rendered. However, if we click on a cell, nothing happens. In the logs we can see "setCell" for each click, but the cell stays blank. The reason here is that nothing tells the cell to re-render. Our state outside of React changes, but React doesn't know about it. That has to change.

至此,我们可以看到正在渲染的字段。 但是,如果单击单元格,则什么也不会发生。 在日志中,我们可以看到每次点击都显示“ setCell”,但是单元格保持空白。 这里的原因是没有任何内容告诉单元重新渲染。 React之外的状态会发生变化,但是React对此一无所知。 那必须改变。

How can we trigger a render programmatically?

我们如何以编程方式触发渲染?

With classes we have forceUpdate. Does it mean we have to re-write our code to classes? Not really. What we can do with functional components is to introduce some dummy state, which we change only to force our component to re-render.

对于类,我们具有forceUpdate 。 这是否意味着我们必须将代码重新编写为类? 并不是的。 我们可以对功能组件执行的操作是引入一些虚拟状态,我们仅对其进行更改以迫使我们的组件重新呈现。

Here's how we can create a custom hook to force re-renders.

这是我们如何创建自定义挂钩以强制重新渲染的方法。

const useForceRender = () => {
  const [, setDummy] = useState(0)
  const forceRender = useCallback(() => setDummy((oldVal) => oldVal + 1), [])
  return forceRender
}

To trigger a re-render when our field updates we have to know when it updates. It means we have to be able to somehow subscribe to field updates.

要在字段更新时触发重新渲染,我们必须知道它何时更新。 这意味着我们必须能够以某种方式订阅现场更新。

class Field {
  constructor(fieldSize) {
    this.size = fieldSize
    this.data = new Array(this.size).fill(new Array(this.size).fill(undefined))
    this.subscribers = {}
  }

  _cellSubscriberId(rowI, cellI) {
    return `row${rowI}cell${cellI}`
  }

  cellContent(rowI, cellI) {
    return this.data[rowI][cellI]
  }

  setCell(rowI, cellI, newContent) {
    console.log('setCell')
    this.data = [
      ...this.data.slice(0, rowI),
      [
        ...this.data[rowI].slice(0, cellI),
        newContent,
        ...this.data[rowI].slice(cellI + 1),
      ],
      ...this.data.slice(rowI + 1),
    ]
    const cellSubscriber = this.subscribers[this._cellSubscriberId(rowI, cellI)]
    if (cellSubscriber) {
      cellSubscriber()
    }
  }

  map(cb) {
    return this.data.map(cb)
  }

  // Note that we subscribe not to updates of the whole filed, but to updates of one cell only
  subscribeCellUpdates(rowI, cellI, onSetCellCallback) {
    this.subscribers[this._cellSubscriberId(rowI, cellI)] = onSetCellCallback
  }
}

Now we can subscribe to field updates.

现在我们可以订阅现场更新。

const Cell = ({ rowI, cellI }) => {
  console.log('cell render')
  const forceRender = useForceRender()
  useEffect(() => field.subscribeCellUpdates(rowI, cellI, forceRender), [
    forceRender,
  ])
  const content = field.cellContent(rowI, cellI)
  return (
    <div onClick={() => field.setCell(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
}

Live demo #7

现场演示#7

Let's play with size with this implementation. Try to increase it to the values that felt laggy before. And… It's time to open a good bottle of champagne! We got ourselves an app that renders one cell and one cell only when the state of that cell changes!

让我们来看看这个实现的size 。 尝试将其增加到以前感觉很落后的值。 而且…是时候打开一瓶好香槟了! 我们得到了一个仅当一个单元格的状态发生变化时才渲染一个单元格和一个单元格的应用程序!

Let's take a look at the DevTools report.

让我们看一下DevTools报告。

React DevTools report #4

As we can see now only Cell is being rendered and it's crazy fast.

正如我们现在所看到的,只有Cell会被渲染,而且速度很快。

What if say that now code of our Cell is a potential cause of a memory leak? As you can see, in useEffect we subscribe to cell updates, but we never unsubscribe. It means that even when Cell is destroyed, its subscription lives on. Let's change that.

如果说现在我们的Cell代码是内存泄漏的潜在原因怎么办? 如您所见,在useEffect我们订阅单元更新,但我们从未取消订阅。 这意味着,即使Cell被销毁,其订阅仍然有效。 让我们改变它。

First, we need to teach Field what it means to unsubscribe.

首先,我们需要告知Field退订的含义。

class Field {
  // ...
  unsubscribeCellUpdates(rowI, cellI) {
    delete this.subscribers[this._cellSubscriberId(rowI, cellI)]
  }
}

Now we can apply unsubscribeCellUpdates to our Cell.

现在我们可以将unsubscribeCellUpdates应用于我们的Cell

const Cell = ({ rowI, cellI }) => {
  console.log('cell render')
  const forceRender = useForceRender()
  useEffect(() => {
    field.subscribeCellUpdates(rowI, cellI, forceRender)
    return () => field.unsubscribeCellUpdates(rowI, cellI)
  }, [forceRender])
  const content = field.cellContent(rowI, cellI)
  return (
    <div onClick={() => field.setCell(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
}

Live demo #8

现场演示#8

So what's the lesson here? When does it make sense to move state down the component tree? Never! Well, not really :) Stick to best practices until they fail and don't do any premature optimizations. Honestly, the case we considered above is somewhat specific, however, I hope you'll recollect it if you ever need to display a really large list.

那么这是什么教训? 什么时候在组件树中向下移动状态? 决不! 好吧,不是真的:)坚持最佳实践,直到它们失败并且不做任何过早的优化。 坦白地说,我们上面考虑的情况有些特定,但是,如果您需要显示非常大的列表,希望您能重新收集它。

奖励步骤:真实世界中的重构 (Bonus step: Real-world refactoring)

In the live demo #8 we used global field, which should not be the case in a real-world app. To solve it, we could host field in our App and pass it down the tree using [context]().

现场演示#8中,我们使用了global field ,在实际应用中情况并非如此。 为了解决这个问题,我们可以在App托管field ,然后使用[context]()将其传递到树上。

const AppContext = createContext()

const App = () => {
  // Note how we used a factory to initialize our state here.
  // Field creation could be quite expensive for big fields.
  // So we don't want to create it each time we render and block the event loop.
  const [field] = useState(() => new Field(size))
  return (
    <AppContext.Provider value={field}>
      <div>
        {field.map((row, rowI) => (
          <div key={rowI}>
            {row.map((cell, cellI) => (
              <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} />
            ))}
          </div>
        ))}
      </div>
    </AppContext.Provider>
  )
}

Now we can consume field from the context in our Cell.

现在我们可以从Cell的上下文中消费field

const Cell = ({ rowI, cellI }) => {
  console.log('cell render')
  const forceRender = useForceRender()
  const field = useContext(AppContext)
  useEffect(() => {
    field.subscribeCellUpdates(rowI, cellI, forceRender)
    return () => field.unsubscribeCellUpdates(rowI, cellI)
  }, [forceRender])
  const content = field.cellContent(rowI, cellI)
  return (
    <div onClick={() => field.setCell(rowI, cellI, randomContent())}>
      {content}
    </div>
  )
}

Live demo #9

现场演示#9

Hopefully, you've found something useful for your project. Feel free to communicate your feedback to me! I most certainly appreciate any criticism and questions.

希望您已经找到了对您的项目有用的东西。 随时向我传达您的反馈! 我非常感谢任何批评和疑问。

翻译自: https://habr.com/en/post/471300/

react状态提升

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值