这个故事是关于无状态组件的 。 这意味着其中没有任何this.state = { ... }
调用的组件。 它们仅处理传入的“道具”和子组件。
首先,超级基础
import React, { Component } from 'react'
class User extends Component {
render() {
const { name, highlighted, userSelected } = this.props
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}
>{name}</h3>
</div>
}
}
编者注:在本文的演示中,我们正在尝试使用CodeSandbox 。
让我们知道您的想法!
好极了! 有用。 这确实很基础,但可以设置示例。
注意事项:
- 它是无状态的。 没有
this.state = { ... }
。 -
console.log
在那里,因此您可以了解其使用情况。 特别是,当您进行性能优化时,您将希望避免在道具没有实际更改的情况下进行不必要的重新渲染。 - 那里的事件处理程序是“内联”的。 这是方便的语法,因为它的代码靠近它处理的元素,加上此语法意味着您不必做任何
.bind(this)
仰卧起坐。 - 使用这样的内联函数,由于必须在每个渲染器上创建该函数,因此性能损失很小。 稍后将详细介绍这一点。
这是一个演示组件
现在我们意识到上面的组件不仅是无状态的,而且实际上是Dan Abramov所谓的表示性组件 。 它只是一个名称,但基本上,它是轻量级的,可以产生一些HTML / DOM,并且不会弄乱任何状态数据。
这样我们就可以使其具有功能! 好极了! 这不仅感觉到“臀部”,而且还使其不那么害怕,因为它更容易推理。 它获取输入,并且与环境无关,始终返回相同的输出。 当然,它可以“回调”,因为其中一个道具是可调用函数。
因此,让我们重新编写它:
const User = ({ name, highlighted, userSelected }) => {
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}>{name}</h3>
</div>
}
感觉不舒服吗? 感觉就像纯JavaScript一样,您无需考虑所使用的框架就可以编写东西。
他们说,它一直在重新渲染:(
假设我们的小User
使用在状态随时间变化的组件中。 但是状态不会影响我们的组件。 例如,如下所示:
import React, { Component } from 'react'
class Users extends Component {
constructor(props) {
super(props)
this.state = {
otherData: null,
users: [{name: 'John Doe', highlighted: false}]
}
}
async componentDidMount() {
try {
let response = await fetch('https://api.github.com')
let data = await response.json()
this.setState({otherData: data})
} catch(err) {
throw err
}
}
toggleUserHighlight(user) {
this.setState(prevState => {
users: prevState.users.map(u => {
if (u.name === user.name) {
u.highlighted = !u.highlighted
}
return u
})
})
}
render() {
return <div>
<h1>Users</h1>
{
this.state.users.map(user => {
return <User
name={user.name}
highlighted={user.highlighted}
userSelected={() => {
this.toggleUserHighlight(user)
}}/>
})
}
</div>
}
}
如果运行此命令,您会注意到即使没有任何更改,我们的小组件也会重新渲染! 现在这不是什么大问题,但是在实际的应用程序中,组件的复杂性趋于增长,并且每次不必要的重新渲染都会使站点变慢。
如果您现在要使用react-addons-perf
调试此应用程序,我相信您会发现浪费时间渲染Users->User
。 不好了! 该怎么办?!
一切似乎都指向一个事实,我们需要使用shouldComponentUpdate
来覆盖React在确定它们没有区别时认为道具不同的方式。 要添加一个React生命周期挂钩,该组件需要成为一个类。 感叹 。 因此,我们回到原始的基于类的实现,并添加新的生命周期挂钩方法:
返回成为类组件
import React, { Component } from 'react'
class User extends Component {
shouldComponentUpdate(nextProps) {
// Because we KNOW that only these props would change the output
// of this component.
return nextProps.name !== this.props.name || nextProps.highlighted !== this.props.highlighted
}
render() {
const { name, highlighted, userSelected } = this.props
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}
>{name}</h3>
</div>
}
}
注意应该添加的shouldComponentUpdate
方法。 这有点丑陋。 我们不仅不能再使用功能,还必须手动列出可能更改的道具。 这涉及到一个大胆的假设,即userSelected
函数prop不会改变。 这不太可能,但是需要提防。
但是请注意,这只会渲染一次! 即使包含的App
组件重新渲染之后。 因此,这对性能有好处。 但是我们可以做得更好吗?
那React.PureComponent呢?
从React 15.3开始,有一个新的组件基类。 它称为PureComponent
,它具有一个内置的shouldComponentUpdate
方法,该方法对每个道具进行“浅相等”比较。 大! 如果使用此方法,则可以丢弃必须列出特定道具的自定义shouldComponentUpdate
方法。
import React, { PureComponent } from 'react'
class User extends PureComponent {
render() {
const { name, highlighted, userSelected } = this.props
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}
>{name}</h3>
</div>
}
}
尝试一下,您会失望的。 每次都会重新渲染。 为什么?! 答案是因为每次在App
的render
方法中都会重新创建功能userSelected
。 这意味着,当基于PureComponent
的组件调用其自己的shouldComponentUpdate()
它将返回true,因为函数每次创建后总是不同的。
通常,解决方案是将函数绑定到包含组件的构造函数中。 首先,如果要这样做,则意味着我们必须键入5次方法名称(而之前是1次):
-
this.userSelected = this.userSelected.bind(this)
(在构造函数中) -
userSelected() {
(作为方法定义本身) -
<User userSelected={this.userSelected} ...
(在定义呈现User
组件的位置时)
如您所见,另一个问题是,当实际执行该userSelected
方法时,它依赖于闭包。 特别是,它依赖于this.state.users.map()
迭代器的作用域变量user
。
诚然,是解决这一并且对第一绑定的userSelected
方法来this
,然后调用该方法(从子组件中)传递用户(或它的名字)回来的时候。 这是一个这样的解决方案 。
recompose
救援!
首先,要迭代,我们想要什么:
- 编写功能组件会更好,因为它们是功能。 这立即告诉代码读取器它没有任何状态。 从单元测试的角度来看,他们很容易推理。 而且,他们觉得JavaScript不太冗长和纯净(当然有了JSX)。
- 我们太懒了,无法绑定所有传递给子组件的方法。 当然,如果方法很复杂,最好将它们重构出来而不是即时创建它们。 即时创建方法意味着我们可以在它们使用的位置附近编写其代码,而不必给它们起一个名字并在3个不同的地方提及5次。
- 除非更改了它们的道具,否则子组件绝对不能重新渲染。 对于微小的快照应用而言,可能并不重要,但是对于现实世界中的应用程序而言,当您拥有很多这样的应用程序时,多余的渲染会在可能的情况下烧毁CPU。
(实际上,我们理想的情况是组件只渲染一次。为什么React无法为我们解决这个问题?那么关于“如何使React快速”的博客文章就会减少90%。)
recompose是“ React实用程序带,用于功能组件和高阶组件。 可以把它想成React的lodash。” 根据文档。 有很多在这个库的探索,但现在我们要显示我们的功能组件,而这些被重新呈现时的道具不会改变。
我们第一次尝试将其重新写回功能组件,但使用recompose.pure
如下所示:
import React from 'react'
import { pure } from 'recompose'
const User = pure(({ name, highlighted, userSelected }) => {
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}>{name}</h3>
</div>
})
export default User
您可能会注意到,如果运行此命令,即使道具( name
和highlighted
键)没有变化, User
组件仍会重新渲染。
让我们把它提高一个档次。 而不是使用recompose.pure
我们将使用recompose.onlyUpdateForKeys
这是一个版本recompose.pure
,但您指定的道具键把重点放在明确:
import React from 'react'
import { onlyUpdateForKeys } from 'recompose'
const User = onlyUpdateForKeys(['name', 'highlighted'])(({ name, highlighted, userSelected }) => {
console.log('Hey User is being rendered for', [name, highlighted])
return <div>
<h3
style={{fontStyle: highlighted ? 'italic' : 'normal'}}
onClick={event => {
userSelected()
}}>{name}</h3>
</div>
})
export default User
运行该命令时,您会注意到,只有在道具name
或highlighted
更改发生时,它才会更新。 如果父组件重新呈现,则User
组件不会重新呈现。
欢呼! 我们找到了金子!
讨论区
首先,问问自己,优化组件是否值得进行性能测试。 也许这比价值还多。 无论如何,您的组件应该轻巧,也许您可以将任何昂贵的计算移出组件,或者将其移至外部可记忆的函数中,或者您可以重新组织组件,以免在某些数据仍然无法使用时不浪费渲染组件。 。 例如,在这种情况下,您可能不希望在完成该fetch
之后才呈现User
组件。
这不是一个坏的解决方案编写代码为您提供最便捷的方式,然后启动你的东西,然后从那里,重复,以使其更好的性能。 在这种情况下,要使功能高效,您需要重写以下内容的功能组件定义:
const MyComp = (arg1, arg2) => {
...
}
…至…
const MyComp = pure((arg1, arg2) => {
...
})
理想情况下,最好的解决方案是显示React的新补丁,而不是展示如何破解事物的方法,这是对shallowEqual
的巨大改进,它能够“自动”破译所传递和比较的内容是功能,并且仅仅因为它不相等并不意味着它实际上是不同的。
入场! 除了必须弄乱构造函数中的绑定方法和每次都重新创建的内联函数外,还有一个中间立场。 这是公共类字段 。 这是Babel stage-2
功能,因此您的设置很可能支持它。 例如, 这是一个使用它的叉子 ,它不仅更短,而且现在也意味着我们不需要手动列出所有非功能性道具。 该解决方案必须放弃关闭。 但是,仍然需要理解并了解recompose.onlyUpdateForKeys
,这是一个很好的需求。
有关React的更多信息,请查看我们的课程React The ES6 Way 。
杰克·富兰克林 ( Jack Franklin )对本文进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
From: https://www.sitepoint.com/optimizing-react-performance-stateless-components/