我正在参加「掘金·启航计划」
React在props或state发生改变时,会重新执行render函数,产生新的DOM树;新旧DOM树会进行diff操作,计算出差异后进行更新。React需要基于这两棵不同的DOM树之间的差别来判断如何有效的更新UI。
如果一棵树完全去参考另一颗DOM树进行比较,即使采用最先进算法,算法的复杂程度仍然是O(n^3) (n代表树中元素的数量)
所以,如果React中采用了这种算法,如果是1000个子元素,那么计算量将在十亿量级,这个开销太昂贵了,更新效率会非常的低。
React中采用了以下算法进行优化,将复杂度降低为O(n):
同层节点比较,不会跨节点比较;
如果产生了不同类型的节点,则不会去进行对比,直接会生成新的树结构;
开发中通过key来指定节点以保证渲染的稳定性;
一、keys的优化
如果在遍历列表时没有指定key,React会报如下警告:
【如果在最后位置插入数据】
- 此时有无key意义并不大
【如果在前面插入数据】
- 在没有key的情况或者key为列表元素的index的话(因为index会发生改变),所有的li都要进行修改
- 如果设置了唯一的值作为key,那么key相同的元素只需要位移即可,不需要进行任何修改,然后将新插入的内容放到最前面即可
key的注意事项
- key应该是唯一的
- 不要使用随机数作为key,因为随机数会在下一次render时重新生成
- 如果使用index作为key,对性能是没有优化的
二、render函数的优化(SCU)
如果在一个父组件中嵌套了两个子组件,父组件中state的数据发生了变化触发了render,此时不管向子组件传递的数据有无发生变化,所有的子组件render函数均会重新执行。
先来看以下案例:
在App中使用了子组件Home与Recommend,其中Home使用App中的message数据、Recommend使用App中的counter数据
App.jsx:
import React, { Component } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'
export class App extends Component {
constructor() {
super()
this.state = {
message: "Hello World",
counter: 0
}
}
changeText() {
this.setState({ message: "Hello React" })
}
increment() {
this.setState({ counter: this.state.counter + 1 })
}
render() {
console.log("App render")
const { message, counter } = this.state
return (
<div>
<h2>App-{message}-{counter}</h2>
<button onClick={e => this.changeText()}>修改文本</button>
<button onClick={e => this.increment()}>counter+1</button>
<Home message={message}/>
<Recommend counter={counter}/>
</div>
)
}
}
export default App
Home.jsx:
import React, { Component } from 'react'
export class Home extends Component {
render() {
console.log("Home render...")
return (
<div>
<h2>Home Page: {this.props.message}</h2>
</div>
)
}
}
export default Home
Recommend.jsx:
import React, { Component } from 'react'
export class Recommend extends Component {
render() {
console.log("Recommend render...")
return (
<div>
<h2>Recommend Page: {this.props.counter}</h2>
</div>
)
}
}
export default Recommend
此时无论是修改文本还是修改数字,Home与Recommend组件均会重新执行render函数
在开发中,只是修改了App中的数据,所有的子组件都要重新进行render,进行diff算法,性能必然会很低:
事实上很多子组件没有必要进行重新render(比如以上案例中,如果只修改了message,Recommend组件是没有必要重新render的,因为Recommend中只使用到了父组件中的counter)
那么如何来控制组件中何时进行重新render呢?
shouldComponentUpdate
我们可以通过类组件中的生命周期函数:shouldComponentUpdate来控制是否重新执行render函数。
该方法有两个参数:
- nextProps 修改之后的最新的props
- nextState 修改之后的最新的state
返回值为boolean类型:
- 返回true则重新执行render
- 返回false则不会重新执行render
- 默认返回true
此时Home组件与Recommend组件中可以优化为:
Home.jsx:
import React, { Component } from 'react'
export class Home extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.message !== this.props.message
}
render() {
console.log("Home render...")
return (
<div>
<h2>Home Page: {this.props.message}</h2>
</div>
)
}
}
export default Home
Recommend.jsx:
import React, { Component } from 'react'
export class Recommend extends Component {
shouldComponentUpdate(nextProps) {
return nextProps.counter !== this.props.counter
}
render() {
console.log("Recommend render...")
return (
<div>
<h2>Recommend Page: {this.props.counter}</h2>
</div>
)
}
}
export default Recommend
可见,修改message时Recommend不再会重新渲染;修改counter时Home不再会重新渲染:
PureComponent
如果所有的组件都需要我们手动实现以下shouldComponentUpdate方法会增加非常多的工作量,事实上React已考虑到了这点,默认帮我们实现好了,我们只需要让类组件继承自PureComponent即可
在PureComponent中会将props与state进行浅层的比较:
示例
import React, { PureComponent } from 'react'
export class Home extends PureComponent {
render() {
console.log("Home render...")
return (
<div>
<h2>Home Page: {this.props.message}</h2>
</div>
)
}
}
export default Home
不再需要手动实现shouldComponentUpdate,直接继承PureComponent即可
memo
shouldComponentUpdate是类组件的生命周期,那么在函数组件中需要通过高阶组件memo来实现props的对比
import { memo } from 'react'
const Home = memo(function(props){
console.log('Home render....')
return (
<div>
<h2>Home Page: { props.message}</h2>
</div>
)
})
export default Home
三、如何正确在PureComponent触发重新渲染
如果一个类组件继承了PureComponent后,在组件中渲染列表结构并对列表中的数据做操作时需要注意了:
由于PureComponent在对比props与state数据时是浅层比较的 , 所以如果直接对数组进行push操作、或者对数组中的某一项数据进行修改,都是不会触发重新render的 (因为数组的地址值并没有发生改变)
错误示范
我们首先来看以下错误的示范:
import React, { PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super()
this.state = {
books: [
{ name: "你不知道JS", price: 99, count: 1 },
{ name: "JS高级程序设计", price: 88, count: 1 },
{ name: "Angular高级设计", price: 78, count: 2 },
{ name: "Vue高级设计", price: 95, count: 3 },
],
}
}
addNewBook() {
const newBook = { name: "React高级设计", price: 88, count: 1 }
this.state.books.push(newBook)
this.setState({ books: this.state.books })
}
addBookCount(index) {
this.state.books[index].count++
}
render() {
const { books } = this.state
return (
<div>
<h2>数据列表</h2>
<ul>
{
books.map((item, index) => {
return (
<li key={index}>
<span>name:{item.name}-price:{item.price}-count:{item.count}</span>
<button onClick={e => this.addBookCount(index)}>+1</button>
</li>
)
})
}
</ul>
<button onClick={e => this.addNewBook()}>添加新书籍</button>
</div>
)
}
}
export default App
可以发现,当点击添加数据与+1操作按钮时页面均无反应
因为这些操作都是在原数组的基础上进行操作的,数组的地址值并没有发生改变,PureComponent中在进行浅层比较的时候会认为state中的数据并未发生改变,所以此时不会重新执行render函数
正确做法
正确的做法是:
- 首先通过 const newArr = [ ...oldArr ] 来创建一个与原数组数据相同的新数组(让地址值发生改变)
- 然后在此数组的基础上进行数组操作
- 最后将操作完的数组通过setState合并进去即可
addNewBook() {
const books = [...this.state.books]
const newBook = { name: "React高级设计", price: 88, count: 1 }
books.push(newBook)
this.setState({ books: books })
}
addBookCount(index) {
const books = [...this.state.books]
books[index].count++
this.setState({ books: books})
}