在 egghead.io 上观看 "Lifting and colocating React State"(The Beginner's Guide to ReactJS 的一部分)。
导致React应用缓慢的主要原因之一是全局状态,尤其是快速变化的状态。请允许我用一个超级假想的例子来说明我的观点,然后我会给你一个稍微现实一点的例子,这样你就可以确定它如何在你自己的应用程序中更实际地应用。
下面是这个例子的代码
function sleep(time) {
const done = Date.now() + time
while (done > Date.now()) {
// sleep...
}
}
// imagine that this slow component is actually slow because it's rendering a
// lot of data (for example).
function SlowComponent({time, onChange}) {
sleep(time)
return (
<div>
Wow, that was{' '}
<input
value={time}
type="number"
onChange={e => onChange(Number(e.target.value))}
/>
ms slow
</div>
)
}
function DogName({time, dog, onChange}) {
return (
<div>
<label htmlFor="dog">Dog Name</label>
<br />
<input id="dog" value={dog} onChange={e => onChange(e.target.value)} />
<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
</div>
)
}
function App() {
// this is "global state"
const [dog, setDog] = React.useState('')
const [time, setTime] = React.useState(200)
return (
<div>
<DogName time={time} dog={dog} onChange={setDog} />
<SlowComponent time={time} onChange={setTime} />
</div>
)
}
玩一玩,你会发现当你与这两个字段互动时,会有一个明显的性能问题。我们可以做各种事情来提高DogName 和SlowComponent组件本身的性能。我们可以拉出像React.memo 这样的渲染保驾护航,并在我们的代码库中所有有缓慢渲染的地方应用。但是我想提出一个替代的解决方案。
如果你还没有读过Colocation,那么我建议你读一读。知道了主机托管可以改善我们的应用程序的维护,让我们尝试一下主机托管一些状态。请注意,time 状态被应用程序中的每个组件使用,这就是为什么它被提升到App 。然而dog 状态只被一个组件使用,所以让我们把这个状态移到主机上(更新的行被突出显示):
function DogName({time}) {
const [dog, setDog] = React.useState('')
return (
<div>
<label htmlFor="dog">Dog Name</label>
<br />
<input id="dog" value={dog} onChange={e => setDog(e.target.value)} />
<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
</div>
)
}
function App() {
// this is "global state"
const [time, setTime] = React.useState(200)
return (
<div>
<DogName time={time} />
<SlowComponent time={time} onChange={setTime} />
</div>
)
}
这就是结果。
哇!现在输入狗的名字已经好得多了。更重要的是,由于集中管理,该组件更容易维护。但它是如何变快的呢?
我听说过这样一句话:让事物变得快速的最好方法是少做一些事情。 这正是这里所发生的事情。当我们在React组件树的高层管理状态时,对该状态的每一次更新都会导致整个React树的无效化。React不知道有什么变化,所以它必须去检查所有的组件以确定它们是否需要DOM更新。这个过程不是免费的(特别是当你有任意慢的组件时)。但如果你像我们在dog状态和DogName 组件那样,将你的状态进一步移到React树下,那么React需要检查的东西就会减少。它甚至懒得调用我们的SlowComponent ,因为它知道那是不可能改变输出的,因为无论如何它都不能引用改变后的状态。
简而言之,以前,当我们改变狗的名字时,每个组件都必须检查是否有变化(重新渲染)。之后,只有DogName 组件需要被检查。这带来了性能上的巨大胜利。很好!
真实世界
我看到这个原则在现实世界的应用中的应用是当人们把东西放到一个全局的Redux商店或在一个全局的环境中,但其实不需要是全局的。像上面的例子中的DogName ,往往是这种perf问题的体现,但我也看到它在鼠标交互上也有很多发生(比如在图表或数据表格上显示工具提示)。
通常人们对这种问题的解决办法是 "去掉 "用户交互(即在应用状态更新之前等待用户停止输入)。这有时是我们能做的最好的办法,但它肯定会导致次优的用户体验(React即将推出的并发模式应该会使这种情况在未来变得不那么必要。请看Dan的这个演示)。
人们尝试的另一个解决方案是应用React的渲染逃生舱,如React.memo 。这在我们设计的例子中效果很好,因为它允许React跳过重新渲染我们的SlowComponent ,但在更实际的情况下,你经常遭受 "千刀万剐 "的痛苦,这意味着没有一个地方真的很慢,所以你最终在所有地方应用React.memo 。当你这样做的时候,你也必须开始到处使用useMemo和useCallback (否则你就会把你在React.memo 上所做的工作全部取消)。这些优化中的每一项都可能解决这个问题,但它大大增加了你的应用程序的代码的复杂性,而且它在解决问题方面实际上不如集中状态有效,因为React仍然需要从顶部运行每个组件来确定它是否应该重新渲染。你肯定会用这种方法运行更多的代码,这是没有办法的事。
什么是集中管理状态?
同位的原则是。
尽可能地将代码放在与之相关的地方
因此,为了达到这个目的,我们把我们的dog 状态放在 DogName 组件内。
function DogName({time}) {
const [dog, setDog] = React.useState('')
return (
<div>
<label htmlFor="dog">Dog Name</label>
<br />
<input id="dog" value={dog} onChange={e => setDog(e.target.value)} />
<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
</div>
)
}
但是,当我们把它拆开时会发生什么?那个状态会去哪里?答案是一样的:"尽可能靠近它的相关位置"。这将是**最接近的共同父体。**举个例子,让我们把DogName 组件拆开,让input 和p 显示在不同的组件中。
function DogName({time}) {
const [dog, setDog] = React.useState('')
return (
<div>
<DogInput dog={dog} onChange={setDog} />
<DogFavoriteNumberDisplay time={time} dog={dog} />
</div>
)
}
function DogInput({dog, onChange}) {
return (
<>
<label htmlFor="dog">Dog Name</label>
<br />
<input id="dog" value={dog} onChange={e => onChange(e.target.value)} />
</>
)
}
function DogFavoriteNumberDisplay({time, dog}) {
return (
<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
)
}
在这种情况下,我们不能把状态移到DogInput 组件,因为DogFavoriteNumberDisplay 需要访问该状态,所以我们在树上导航,直到找到这两个组件的最小共同父级,这就是管理状态的地方。
这和你的状态需要在你的应用程序的一个特定屏幕上的几十个组件中被访问一样适用。如果你愿意,你甚至可以把它放到上下文中,以避免道具的钻取。但是,让上下文值提供者尽可能靠近它的相关位置,你仍然会从主机托管的性能(和维护)特性中受益。我的意思是,虽然你的一些上下文提供者可以在你的应用程序的React树的顶部呈现,但它们不一定都在那里。你可以把它们放在最合理的地方。
这就是我的React应用状态管理博文的精髓所在。让你的状态尽可能地靠近它的使用位置,你会从维护的角度和性能的角度受益。 从那里,你应该有的唯一的性能问题是偶尔的特别复杂的UI交互。
那么上下文或Redux呢?
如果你读过《优化React重现的一个简单技巧》,那么你就知道你可以让它只更新那些真正使用变化状态的组件。所以这可以从侧面解决这个问题。虽然这是真的,但人们在使用Redux时确实仍有性能问题。如果不是React本身的问题,那是什么问题呢?问题是React-Redux希望你遵循准则,避免对连接的组件进行不必要的渲染,而当其他全局状态发生变化时,很容易不小心设置的组件渲染过于频繁。随着你的应用越来越大,这种影响会越来越严重,尤其是当你把太多的状态放到Redux中时。
幸运的是,你可以做一些事情来帮助减少这些性能问题的影响,比如使用备忘录化的重选选择器来优化mapState ,Redux文档中有关于提高Redux应用性能的额外信息。
我还想指出,你绝对可以用Redux应用colocation来获得这些好处。只要将你存储在Redux中的东西限制为实际的全局状态,并将其他所有的东西放在一起,你就可以了。Redux FAQ有一些经验法则,可以帮助决定状态是应该放在Redux中,还是留在组件中。
此外,如果你把状态按领域分开(有多个特定领域的上下文),那么这个问题也就不那么明显了。
但事实是,如果你把你的状态放在一起,你就不会有这些问题,维护也会得到改善。
那么,你如何决定把状态放在哪里呢?
我做了这个决策树图来帮助。

下面是写出来的(为读屏者和朋友)
- 1 开始构建一个应用程序。转到2
- 2 使用状态。转到3
- 3 只由这个组件使用?
- 是:转到4
- 不:只有一个孩子使用?
- 是:定位状态。转到3
- 不:由兄弟姐妹/父母使用?
- 是:提升状态。转到3
- 不:转到4
- 4 离开它。转到5
- 5有一个 "道具钻孔 "的问题?
- 是:子代可以在父代之外发挥作用吗?
- 是:将状态移至上下文提供者。转到6
- 不:使用组件构成。转到6
- 否:转到6
- 是:子代可以在父代之外发挥作用吗?
- 6 发送应用程序。随着需求的变化,转到1
重要的是,这是你作为常规重构/应用维护过程的一部分所做的事情。这是因为提升状态是让它工作的一个要求,所以它自然而然地发生了,但无论你是否将你的状态放在一起,你的应用程序都会 "工作",所以有意识地考虑这个问题对于保持你的应用程序可管理和快速是很重要的。
如果你想了解更多关于组件组成的步骤,请阅读One React mistake that's slowing you down中的内容。
总结
一般来说,我认为人们很擅长在事情发生变化时 "提升状态",但我们并不经常考虑在代码库发生变化时 "定位 "状态。 因此,我对你的挑战是查看你的代码库并寻找定位状态的机会。问问自己 "我真的需要把模态的status (打开/关闭)状态放在Redux中吗?"(答案可能是 "不")。把你的状态放在一起,你会发现自己的代码库更快、更简单。 祝你好运