我在准备一篇与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 就我们所知,唯一的变化是onClick 和children 这个元素的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对象。
那么这对我来说意味着什么呢?
总之,如果你遇到了性能问题,可以试试这个:
- 将昂贵的组件 "提升 "到一个父级,在那里它被渲染的频率会更低。
- 然后将昂贵的组件作为一个道具传递下去。
你可能会发现这样做可以解决你的性能问题,而不需要像一个巨大的侵入性创可贴一样在你的代码库中到处传播React.memo 🤕😉
演示
在React中创建一个实用的慢速应用程序的演示是很棘手的,因为它有点需要建立一个完整的应用程序,但我确实有一个精心设计的应用程序的例子,有一个之前/之后,你可以检查和玩。
我想补充的一点是,尽管使用这段代码的快速版本更好,但它在最初渲染时的表现仍然非常糟糕,如果它真的需要再做一次自上而下的重新渲染(或者当你更新行/列时),它的表现也会非常糟糕。这是一个性能问题,也许应该根据其本身的优点来处理(不管重新渲染的必要性如何)。另外,请记住,codesandbox使用的是React的开发版本,它给你带来了非常好的开发体验,但性能比React的生产版本慢得多。
这也不仅仅是在你的应用程序的顶层有用的东西,这可以应用到你的应用程序的任何地方,它是有意义的。我喜欢这一点的原因是:"它既是自然的构成*,又*是一个优化的机会。"(这是丹的说法)。所以我自然而然地做这件事,并免费获得香水的好处。这也是我一直喜欢React的原因。React是这样写的:默认情况下,习惯性的React应用程序是快速的,然后React提供了优化助手,供你作为逃生舱口使用。
祝您好运!