PureComponent内部实现原理是什么?

前言

性能优化一直是前端热于讨论的话题,我们可以根据不同的场景、框架来做不同的措施,而React框架的特点是“可玩性高”,他只提供简单的响应式封装和JSX语法糖,而给我们大量的优化空间,今天我们介绍一个非常强大的属性:PureComponent

DEMO

在开始之间,我们看一段代码,他或许能给我们一些思考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';

class Item extends React.Component {
render() {
const { num } = this.props
console.log('Item Render');
return(
<p>{num}</p>
)
}
}

class App extends React.Component{
state = {
list: Array(100).fill(0).map((val, index) => ({id: index}))
}
handleClick = () => {
this.setState({
num: 1
})
}
render() {
const { list } = this.state;
console.log('App Render');
return(
<div>
<button onClick={this.handleClick}>Click</button>
{list.map((v,index)=>(
<Item key={index} num={v.id} />
))}
</div>
)
}
}

export default App

当我们点击Click的时候。App组件state中的num发生变化,然后触发了App的render,有因为Item组件为App的子组件,所以Item的render也需要触发:

image

我们思考,子组件Item中的props并没有发生变化,也没必要去render一次,更没必要去触发Item组件的对比virtual DOM,这个时候,我们或许想起了是否能利用shouldComponentUpdate这个生命周期来去组织Item组件的render触发,其实PureComponent就是利用这个生命周期函数提高组件的性能。

PureComponent

PureComponent是React15.3中新加的一个类,顾名思义, pure 是纯的意思,PureComponent 也就是纯组件,取代其前身 PureRenderMixin , PureComponent 是优化 React 应用重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render 操作的次数,从而提高性能。

原理

我们先来理解React响应式的原理:

  1. 组件state或props改变
  2. 通过shouldComponentUpdate来判断是否需要update
  3. 如果需要,则进行render()
  4. 根据具state、props得到virtual DOM,与之前的virtual DOM对比
  5. virtual DOM改变,则渲染新的DOM

我们看到,从第二步开始已经在进行密集的计算了,假如要提升性能,这可以从两个角度去优化:

  1. 不要触发 render function
  2. 保持 virtual DOM 的一致

最理想的情况是组织触发没必要的render function,比如最上面的Item组件,而PureComponent的原理就是当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较,我们可以查看这部分的源码:

1
2
3
4
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps)
|| !shallowEqual(inst.state, nextState);
}

而 shallowEqual 会比较 Object.keys(state | props) 的长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的,因此,我们使用PureComponent的时候尤其要注意场景:组件state、props不容易发生变化

实践

我们继续改造上面的DEMO例子:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';

class Item extends React.PureComponent {
render() {
const { num } = this.props
console.log('Item Render');
return(
<p>{num}</p>
)
}
}
...

当我们改变父组件的state后,子组件Item因为添加了PureComponent属性,则会自动去判断props、state前后是否一致,一致则不触发render,所以这次我们看到:

image

避免不必要的render对组件能带来较大的性能提升,但是PureComponent并不是适应任何场景的。

PureComponent陷阱

为什么说PureComponent有陷阱,那就是因为PureComponent的默认的shouldComponentUpdate只是做了一层浅对比,我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react'

class App extends React.PureComponent{
state = {
list: [1,2,3,4]
}
handleClick = () => {
const { list } = this.state;
list.push(5);
this.setState({
list
})
}
render(){
const { list } = this.state
return (
<div>
<button onClick={this.handleClick}>Click</button>
{list.map((v,index) => (
<p key={index}>{v}</p>
))}
</div>
)
}
}

export default App

这里,我们点击Click,改变state,但是view并没有发生改变,这是为什么呢?

因为shallowEqual只是一层浅比较,当它判断他们指向的内存是一致的,那么就shouldComponentUpdate就直接返回false,render就不渲染了,因此,我们非常不建议在组件的props或者state易变的情况下使用PureComponent。

那么我们如何解决呢?

1
2
3
4
5
6
handleClick = () => {
const { list } = this.state;
this.setState({
list: [...list, 5]
})
}

只需要创建一个新的数组,再赋值给list,这样shallowEqual检测到他们并不是指向同一块内存,则会触发render。

在PureComponent改动一个state需要这样才能生效,那我们思考,在这种情况下,是否真的需要使用PureComponent?他会给我们带来性能提升吗?

答案是否定的:

我们来看一下,当组件的state、props易变的情况下,使用PureComponent会经历一个怎样的过程:

  1. state发生改变
  2. shouldComponentUpdate函数通过shallowEqual来判断prev与next是否一致
  3. 判断不一致,触发render

在state、props易变的组件,我们使用PureComponent,每次改变都需要经过一层shouldComponentUpdate里面的shallowEqual,这显然是对性能不友好的,所以这是一个PureComponent陷阱,我们一定要根据组件的条件来选择是否使用PureComponent,如果胡乱使用PureComponent,不仅代码可能会出bug,还会造成“性能反优化”的尴尬场景。

最后

当我们使用一个新的特性的时候,一定要了解其使用的场景,冒着侥幸的心理去做一件事肯定会适得其反,PureComponent只是适合用在简单、状态不易变的组件当中。

To save time is to lengthen life. :)