这篇文章是关于状态 共置的 ,也就是说,关于状态的共置 ,这个术语也可以翻译成状态共置或状态共置 。
降低React应用程序速度的主要原因之一是其全局状态。 我将通过一个非常简单的应用程序示例来说明这一点,之后,我将给出一个更接近真实生活的示例。
这是一个简单的应用程序,您可以在其中输入狗的名字(如果窗口不起作用, 则为链接 ):
如果您使用此应用程序,很快就会发现它的运行非常缓慢。 与任何输入字段进行交互时,都会出现明显的性能问题。 在这种情况下,您可以使用React.memo
形式的救生圈,并用慢速渲染将其包裹在所有组件中。 但是,让我们尝试以不同的方式解决这个问题。
这是此应用程序的代码:
function sleep(time) { const done = Date.now() + time while (done > Date.now()) { // ... } } // // - 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() { // " " (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> ) }
如果您尚未阅读有关托管的文章( 托管 ),则建议您阅读。 知道共同托管可以使我们的应用程序更容易使用,让我们在处理状态时使用此原理。
请注意我们应用程序的代码,即time
状态-它由我们应用程序的每个组件使用,因此将其提升( 提升-提升状态 )到App
组件(包装整个应用程序的组件)。 但是,“ dog”状态( dog
和setDog
)仅由一个组件使用, App
组件中不需要它,因此让我们将其移至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> ) } function App() { // " " (global state) const [time, setTime] = React.useState(200) return ( <div> <DogName time={time} /> // <- <SlowComponent time={time} onChange={setTime} /> </div> ) }
这是我们的结果(如果窗口不起作用,请链接 ):
哇! 现在输入名称的速度更快。 此外,由于位于同一位置,因此组件变得更易于维护。 但是为什么它能更快地工作呢?
他们说,快速做某事的最好方法是尽可能少地做某事。 这正是这里发生的事情。 当我们管理位于组件树顶部的状态时,此状态的每次更新都会使整个树无效。 React不知道发生了什么变化,因此,他必须检查所有组件以查看它们是否需要DOM更新。 此过程不是免费的,并且会消耗资源(尤其是如果您故意降低组件速度)。 但是,如果您将状态在组件树中移到尽可能低的位置(如我们对dog
状态和DogName
组件所做的DogName
,那么React将执行较少的检查。 React不会检查SlowComponent
组件(我们故意使它变慢),因为React知道该组件仍然不会影响输出。
简而言之,在更早的时候,更改狗的名称时,会检查每个组件的更改(重新渲染)。 在对代码进行更改之后,React开始只检查DogName
组件。 这大大提高了生产率!
在现实生活中
我看到开发人员将Redux全局存储库或全局上下文中的内容放到了真正不应该是全局的位置。 上面示例中的DogName
通常是发生性能问题的地方。 我经常看到与鼠标进行交互时(例如,在图形上方或数据表上方显示工具提示时)会出现此问题。
解决此问题的一种方法是“取消”用户交互(也就是说,我们等到用户停止键入,然后再应用状态更新)。 有时候,这是我们所能做到的最好的选择,但它可能导致不良的用户体验(未来的并发模式应将这样做的必要性降至最低。 请参见Dan Abramov的演示 )。
开发人员经常使用的另一种解决方案是使用React救援渲染之一,例如React.memo
。 这将在我们牵强的示例中很好地工作,因为它允许React跳过SlowComponent
的重新渲染,但实际上,应用程序可能会因为“千次切割的死亡”而遭受损失,因为在实际应用程序中,减速通常不是由于一个慢组件,并且由于许多组件的工作速度不够快,因此您必须在各处使用React.memo
。 完成此操作后,您将必须开始使用useMemo
和useCallback
,否则您放入React.memo
所有工作都是徒劳的。 这些动作可能解决了问题,但是它们明显增加了代码的复杂度,实际上,它们仍然比联合状态放置的效率低,因为React需要遍历每个组件(从顶部开始)以确定是否再次渲染它。
如果您想使用一个牵强一些的示例, 请转到此处的codeandbox 。
什么是主机代管?
联合安置的原则规定:
该代码应放置在尽可能靠近它所涉及的地方。
因此,为了遵守此原则,我们的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
组件, DogName
在不同的组件p
显示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的应用程序状态管理的主要思想。 使您的状态尽可能地靠近使用它们的地方,这将提高代码的性能和可用性。 使用这种方法,唯一可能使应用程序性能下降的是与接口元素的特别复杂的交互。
那么要使用什么,上下文还是Redux?
如果您阅读了“优化React重新渲染的一个简单技巧” ,那么您会知道可以确保仅更新那些使用更改状态的组件。 这样,您可以解决此问题。 但是,使用编辑器时,人们仍然会遇到性能问题。 问题是React-Redux希望您遵循其建议,以避免不必要地渲染连接的组件 。 总是有可能出错;您可能会无意中配置组件,以便在其他全局状态更改时,它们开始变得过于频繁。 而且,应用程序越大,其效果越负面,尤其是在向编辑器添加过多状态的情况下。
有一些方法可以帮助您避免这些问题,例如,使用memoized Reselect mapState
,或者阅读Editors文档以获取有关提高应用程序性能的更多信息 。
值得注意的是,共同放置可以与编辑器一起应用。 仅将编辑器仅用于全局状态,对于其他所有状态,都使用同一位置。 编辑器的常见问题解答中有一些有用的规则,可帮助您确定状态应在编辑器中起作用还是应保留在组件中 。
顺便说一句,如果您将状态划分为多个域(根据域使用多个上下文),那么问题将不那么明显。
但是事实仍然存在:状态的并置减少了性能问题并简化了代码维护。
决定放置状态
决策树:

文本版本,如果无法查看图片:
- 1我们开始开发应用程序。 转到2
- 2组件中的状态。 转到3
- 3条件是否仅由该组件使用?
- ?? 我们去4
- 不行吗 此状态是否仅需要 一个子组件?
- ?? 将其移动到此子组件(使用同一位置)。 我们去3。
- 不行吗 此状态是否需要父组件或相邻组件(“兄弟”组件,即同一父组件的子组件)?
- ?? 将上面的状态移动到父组件。 转到3
- 不行吗 我们去4
- 4保持原样。 转到5
- 5螺旋钻有问题吗?
- ?? 我们将此状态移到上下文提供程序,并在管理状态的组件中呈现此提供程序。 转到6
- 不行吗 转到6
- 6我们发送申请。 出现新要求时,转到1
重要的是,此过程应成为常规应用程序重构/维护过程的一部分。 如果您的病情没有在应引起的地方升高,则此状态将停止正常工作,并且您会注意到它。 但是,如果您不遵循状态共置方法,并且不降低组件层次结构中较低的状态,则您的应用程序将继续运行。 因此,您不会立即注意到将逐渐累积的性能和可管理性问题。
结论
通常,开发人员非常了解何时需要在必要时升高状态(“提升状态”),但是对于何时需要降低状态,我们的理解不是那么清楚。 我建议您看一下应用程序的代码,并考虑通过应用“共置位”原则可以在何处省略状态。 问问自己:“我需要编辑器中模式窗口的isOpen
状态吗?” (答案很可能是否)。
使用共置原则,您的代码将变得越来越容易。
祝你好运