react状态提升
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个主要概念之一可能会导致性能下降吗? 在本文中,我们将考虑实际情况。
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 (
{field.map((row, rowI) => (
{row.map((cell, cellI) => (
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),
])
}
/>
))}
|
))}
)
}
A child component to display a state of a single cell.
显示单个单元格状态的子组件。
const randomContent = () => (Math.random() > 0.5 ? 'x' : '0')
const Cell = ({ content, setContent }) => (
setContent(randomContent())}>{content}
)
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 setContent(randomContent())}>{content}
}
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文档的建议添加一些密钥。
{field.map((row, rowI) => (
{row.map((cell, cellI) => (
setField([
...field.slice(0, rowI),
[
...field[rowI].slice(0, cellI),
newContent,
...field[rowI].slice(cellI + 1),
],
...field.slice(rowI + 1),
])
}
/>
))}
|
))}
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的帮助下获得帮助。 它能够记录为什么渲染组件。 但是,您必须手动启用它。
Once it's enabled, we can see that all cells were re-rendered because their props changed, specifically, setContent
prop.
启用后,我们可以看到所有单元均被重新渲染,因为它们的道具已更改,特别是setContent
道具。
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.
每个单元格都有两个道具: content
和setContent
。 如果单元格[0] [0]更改,则单元格[0] [1]的内容不变。 另一方面, setContent
在其闭包中捕获field
, cellI
和rowI
。 cellI
和rowI
保持不变,但是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 setState
— it accepts callbacks.
为了使对setContent
的引用相同,我们应该删除闭包。 我们可以通过使Cell
显式地将cellI
和rowI
传递给setContent
来消除cellI
和rowI
闭包。 至于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
看起来像这样
{field.map((row, rowI) => (
{row.map((cell, cellI) => (
|
))}
))}
Now Cell
has to pass cellI
and rowI
to the setContent
.
现在, Cell
必须将cellI
和rowI
传递给setContent
。
const Cell = ({ content, rowI, cellI, setContent }) => {
console.log('cell render')
return (
setContent(rowI, cellI, randomContent())}>
{content}
)
}
Live demo #4
现场演示#4
Let's take a look at the DevTools report.
让我们看一下DevTools报告。
What?! Why the heck does it say "parent props changed"? So the thing is that every time our field is updated App
is 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
或其近亲: PureComponent
和memo
。
const Cell = memo(({ content, rowI, cellI, setContent }) => {
console.log('cell render')
return (
setContent(rowI, cellI, randomContent())}>
{content}
)
})
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报告。
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
倍!
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
。 如果继续使用useState
在App
托管我们的州是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 (
{// As you can see we still need to iterate over our state to get indexes.
field.map((row, rowI) => (
{row.map((cell, cellI) => (
|
))}
))}
)
}
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 (
field.setCell(rowI, cellI, randomContent())}>
{content}
)
}
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 (
field.setCell(rowI, cellI, randomContent())}>
{content}
)
}
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报告。
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 (
field.setCell(rowI, cellI, randomContent())}>
{content}
)
}
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.
那么这是什么教训? 什么时候在组件树中向下移动状态? 决不! 好吧,不是真的:)坚持最佳实践,直到它们失败并且不做任何过早的优化。 坦白地说,我们上面考虑的情况有些特定,但是,如果您需要显示非常大的列表,希望您能重新收集它。
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 (
{field.map((row, rowI) => (
{row.map((cell, cellI) => (
|
))}
))}
)
}
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 (
field.setCell(rowI, cellI, randomContent())}>
{content}
)
}
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状态提升