博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React 性能优化大挑战:一次理解 Immutable data 跟 shouldComponentUpdate
阅读量:6624 次
发布时间:2019-06-25

本文共 11471 字,大约阅读时间需要 38 分钟。

前阵子正在重构公司的专案,试了一些东西之后发现自己对于 React 的渲染机制其实不太了解,不太知道 render 什么时候会被触发。而后来我发现不只我这样,其实还有满多人对这整个机制不太熟悉,因此决定写这篇来分享自己的心得。其实不知道怎么优化倒还好,更惨的事情是你自以为在优化,其实却在拖慢效能,而根本的原因就是对 React 的整个机制还不够熟。被「优化」过的 component 反而还变慢了!这个就严重了。因此,这篇文章会涵盖到下面几个主题:

● Component 跟 PureComponent 的差异
● shouldComponentUpdate 的作用
● React 的渲染机制
● 为什么要用 Immutable data structures

为了判别你到底对以上这些理解多少,我们马上进行几个小测验!有些有陷阱,请睁大眼睛看清楚啦!

React 小测验

第一题

以下程式码是个很简单的网页,就一个按钮跟一个叫做Content的元件而已,而按钮按下去之后会改变App这个 component 的 state。

class Content extends React.Component {render () {console.log('render content!');return 
Content
}}class App extends React.Component {handleClick = () => {this.setState({a: 1})}render() {console.log('render App!');return (
);}}ReactDOM.render(
,document.getElementById('container'));

请问:当你按下按钮之后,console 会输出什么?

A. 什么都没有(App 跟 Content 的 render function 都没被执行到)

B. 只有 render App!(只有 App 的 render function 被执行到)

C. render App! 以及 render content!(两者的 render function 都被执行到)

第二题

以下程式码也很简单,分成三个元件:App、Table 跟 Row,由 App 传递 list 给 Table,Table 再用 map 把每一个 Row 都渲染出来。

class Row extends Component {render () {const {item, style} = this.props;return ({item.id})}}class Table extends Component {render() {const {list} = this.props;const itemStyle = {color: 'red'}return (
{list.map(item => )}
)}}class App extends Component {state = {list: Array(10000).fill(0).map((val, index) => ({id: index}))}handleClick = () => {this.setState({otherState: 1})}render() {const {list} = this.state;return (
);}}

而这段程式码的问题就在于按下按钮之后,App的 render function 被触发,然后Table的 render function 也被触发,所以重新渲染了一次整个列表。

可是呢,我们点击按钮之后,list根本没变,其实是不需要重新渲染的,所以聪明的小明把 Table 从 Component 变成 PureComponent,只要 state 跟 props 没变就不会重新渲染,变成下面这样:

class Table extends PureComponent {render() {const {list} = this.props;const itemStyle = {color: 'red'}return (
{list.map(item => )}
)}}// 不知道什么是 PureComponent 的朋友,可以想成他自己帮你加了下面的 functionshouldComponentUpdate (nextProps, nextState) {return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)}

把 Table 从 Component 换成 PureComponent 之后,如果我们再做一次同样的操作,也就是按下change state按钮改变 App 的 state,这时候会提升效率吗?

A. 会,在这情况下 PureComponent 会比 Component 有效率

B. 不会,两者差不多

C. 不会,在这情况下 Component 会比 PureComponent 有效率

第三题

接著让我来看一个跟上一题很像的例子,只是这次换成按按钮以后会改变 list:

class Row extends Component {render () {const {item, style} = this.props;return ({item.id})}}class Table extends PureComponent {render() {const {list} = this.props;const itemStyle = {color: 'red'}return (
{list.map(item => )}
)}}class App extends Component {state = {list: Array(10000).fill(0).map((val, index) => ({id: index}))}handleClick = () => {this.setState({list: [...this.state.list, 1234567] // 增加一个元素})}render() {const {list} = this.state;return (
);}}

这时候 Table 的 PureComponent 优化已经没有用了,因为 list 已经变了,所以会触发 render function。要继续优化的话,比较常用的手段是把 Row 变成 PureComponent,这样就可以确保相同的 Row 不会再次渲染。

class Row extends PureComponent {render () {const {item, style} = this.props;return ({item.id})}}class Table extends PureComponent {render() {const {list} = this.props;const itemStyle = {color: 'red'}return (
{list.map(item => )}
)}}

请问:把 Row 从 Component 换成 PureComponent 之后,如果我们再做一次同样的操作,也就是按下change state按钮改变 list,这时候会提升效率吗?

A. 会,在这情况下 PureComponent 会比 Component 有效率

B. 不会,两者差不多

C. 不会,在这情况下 Component 会比 PureComponent 有效率

React 的 render 机制

在公布答案之前,先帮大家简单複习一下 React 是如何把你的画面渲染出来的。

首先,大家都知道你在render这个 function 裡面可以回传你想渲染的东西,例如说

class Content extends React.Component {render () {return 
Content
}}

要注意的是这边 return 的东西不会直接就放到 DOM 上面去,而是会先经过一层 virtual DOM。其实你可以简单把这个 virtual DOM 想成 JavaScript 的物件,例如说上面 Content render 出来的结果可能是:

{tagName: 'div',children: 'Content'}

最后一步则是 React 进行 virtual DOM diff,把上次的跟这次的做比较,并且把变动的部分更新到真的 DOM 上面去。

简单来说呢,就是在 React Component 以及 DOM 之间新增了一层 virtual DOM,先把你要渲染的东西转成 virtual DOM,再把需要更新的东西 update 到真的 DOM 上面去。

如此一来,就能够减少触碰到真的 DOM 的次数并且提升性能。

举个例子,假设我们实作一个非常简单的,按一个按钮之后就会改变 state 的小范例:

class Content extends React.Component {render () {return 
{this.props.text}
}}class App extends React.Component {state = {text: 'hello'}handleClick = () => {this.setState({text: 'world'})}render() {return (
);}}

在程式刚开始执行时,渲染的顺序是这样的:

呼叫 App 的 render

呼叫 Content 的 render

拿到 virtual DOM

跟上次的 virtual DOM 做比较

把改变的地方应用到真的 DOM

这时候的 virtual DOM 整体应该会长得像这样:

{tagName: 'div',children: [{tagName: 'button',children: 'setState'}, {tagName: 'div',children: 'hello'}]}

当你按下按钮,改变 state 了以后,执行顺序都跟刚刚一样:

呼叫 App 的 render

呼叫 Content 的 render

拿到 virtual DOM

这时候拿到的 virtual DOM 应该会长得像这样:

{tagName: 'div',children: [{tagName: 'button',children: 'setState'}, {tagName: 'div',children: 'world' // 只有这边变了}]}

而 React 的 virtual DOM diff 演算法,就会发现只有一个地方改变,然后把那边的文字替换掉,其他部分都不会动到。

其实官方文件把这一段写得很好:

When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.

大意就是你可以想像成 render function 会回传一个 React elements 的 tree,然后 React 会把这次的 tree 跟上次的做比较,并且找出如何有效率地把这差异 update 到 UI 上面去。

所以说呢,如果你要成功更新画面,你必须经过两个步骤:

render function

virtual DOM diff

因此,要优化效能的话你有两个方向,那就是:

不要触发 render function

保持 virtual DOM 的一致

我们先从后者开始吧!

提升 React 效能:保持 virtual DOM 的一致

因为有了 virtual DOM 这一层的守护,通常你不必太担心 React 的效能。

像是我们开头问答的第一题:

class Content extends React.Component {render () {console.log('render content!');return 
Content
}}class App extends React.Component {handleClick = () => {this.setState({a: 1})}render() {console.log('render App!');return (
);}}ReactDOM.render(
,document.getElementById('container'));

你每次按下按钮之后,由于 App 的 state 改变了,所以会先触发 App 的 render function,而因为裡面有回传<Content />,所以也会触发 Content 的 render function。

因此你每按一次按钮,这两个 component 的 render function 就会个别被呼叫一次。所以答案是C. render App! 以及 render content!(两者的 render function 都被执行到)

可是尽管如此,真的 DOM 不会有任何变化。因为在 virtual DOM diff 的时候,React 会发现你这次跟上次的 virtual DOM 长得一模一样(因为没有东西改变嘛),就不会对 DOM 做任何操作。

如果能尽量维持 virtual DOM 的结构相似的话,可以减少一些不必要的操作,在这点上其实可以做的优化还很多,可以参考官方文件,裡面写的很详细。

提升 React 效能:不要触发 render function

虽然不必太过担心,但是 virtual DOM diff 也是需要执行时间的。虽然说速度很快,但再快也比不上完全不呼叫来的快,你说是吧。

对于这种「我们已经明确知道不该有变化」的情形,我们连 render 都不该呼叫,因为没必要嘛,再怎么呼叫都是一样的结果。如果 render 没有被呼叫的话,连 virtual DOM diff 都不需要执行,又提升了一些性能。

你应该有听过shouldComponentUpdate这个 function,就是来做这件事的。如果你在这个 function 中回传 false,就不会重新呼叫 render function。

class Content extends React.Component {shouldComponentUpdate () {return false;}render () {console.log('render content!');return 
Content
}}class App extends React.Component {handleClick = () => {this.setState({a: 1})}render() {console.log('render App!');return (
);}}

加上去之后,你会发现无论你按多次按钮,Content 的 render function 都不会被触发。

但是这个东西请小心使用,一个不注意你就会碰到 state 跟 UI 搭不上的情形,例如说 state 明明变成 world,可是 UI 显示的还是 Hello:

class Content extends React.Component {shouldComponentUpdate(){return false;}render () {return 
{this.props.text}
}}class App extends React.Component {state = {text: 'hello'}handleClick = () => {this.setState({text: 'world'})}render() {return (
);}}

在上面的例子中,按下按钮之后 state 确实变成world,但是因为 Content 的shouldComponentUpdate永远都回传 false,所以不会再次触发 render,就看不到对应的新的 state 的画面了。

不过这有点极端,因为通常不会永远都回传 false,除非你真的确定这个 component 完全不需要 re-render。

比起这个,有一个更合理的判断基准是:

如果每一个 props 跟 state 都没有变,那就回传 false

class Content extends React.Component {shouldComponentUpdate(nextProps, nextState){return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);}render () {return 
{this.props.text}
}}

假设this.props是:

{text: 'hello'}

而nextProps是:

{text: 'world'}

那在比较的时候就会发现props.text变了,就可以顺理成章的呼叫 render function。还有另外一点是这边用shallowEqual来比较前后的差异,而不是用deepEqual。

这是出于效能上的考量。别忘了,你要执行这样的比较也是会吃资源的,尤其是在你的 object 很深很深的时候,要比较的东西可就多了,因此我们会倾向用shallowEqual,只要比较一层即可。

另外,前面有提到PureComponent这个东西,其实就是 React 提供的另外一种元件,差别就是在于它自动帮你加上上面那一段的比较。如果你想看原始码的话,在这边:

if (type.prototype && type.prototype.isPureReactComponent) {return (!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState));}

讲到这边,就可以来公佈第二题的解答了,答案是:A. 会,在这情况下 PureComponent 会比 Component 有效率,因为继承了 PureComponent 之后,只要 props 跟 state 没变,就不会执行 render function,也不会执行 virtual DOM diff,节省了许多开销。

shallowEqual 与 Immutable data structures

你刚开始在学 React 的时候,可能会被告诫说如果要更改资料,不能够这样写:

// 不能这样const newObject = this.state.objnewObject.id = 2;this.setState({obj: newObject})// 也不能这样const arr = this.state.arr;arr.push(123);this.setState({list: arr})

而是应该要这样:

this.setState({obj: {...this.state.obj,id: 2}})this.setState({list: [...this.state.arr, 123]})

但你知道为什么吗?

这个就跟我们上面讲到的东西有关了。如同上面所述,其实使用PureComponent是一件很正常的事情,因为 state 跟 props 如果没变的话,本来就不该触发 render function。

而刚刚也提过PureComponent会帮你shallowEqual state 跟 props,决定要不要呼叫 render function。

在这种情况下,如果你用了一开始讲的那种写法,就会产生问题,例如说:

const newObject = this.state.objnewObject.id = 2;this.setState({obj: newObject})

在上面的程式码中,其实this.state.obj跟newObject还是指向同一个物件,指向同一块记忆体,所以当我们在做shallowEqual的时候,就会判断出这两个东西是相等的,就不会执行 render function 了。

在这时候,我们就需要 Immutable data,Immutable 翻成中文就是永远不变的,意思就是:「当一个资料被创建之后,就永远不会变了」。那如果我需要更改资料的话怎么办呢?你就只能创一个新的。

const obj = {id: 1,text: 'hello'}obj.text = 'world' // 这样不行,因为你改变了 obj 这个物件// 你必须要像这样创造一个新的物件const newObj = {...obj,text: 'world'}

有了 Immutable 的概念之后,shallowEqual就不会出错了,因为如果我们有新的资料,就可以保证它是一个新的 object,这也是为什么我们在用setState的时候总是要产生一个新的物件,而不是直接对现有的做操作。

/ 没有 Immutable 的概念前const props = {id: 1,list: [1, 2, 3]}const list = props.list;list.push(4)nextProps = {...props,list}props.list === nextProps.list // true// 有了 Immutable 的概念后const props = {id: 1,list: [1, 2, 3]}const nextProps = {...props,list: [...props.list, 4]}props.list === nextProps.list // false

PureComponent 的陷阱

当我们遵守 Immutable 的规则之后,理所当然的就会想把所有的 Component 都设成 PureComponent,因为 PureComponent 的预设很合理嘛,资料没变的话就不呼叫 render function,可以节省很多不必要的比较。

那让我们回头来看开场小测验的最后一题:

class Row extends PureComponent {render () {const {item, style} = this.props;return ({item.id})}}class Table extends PureComponent {render() {const {list} = this.props;const itemStyle = {color: 'red'}return (
{list.map(item => )}
)}}

我们把Row变成了 PureComponent,所以只要 state 跟 props 没变,就不会 re-render,所以答案应该要是A. 会,在这情况下 PureComponent 会比 Component 有效率?

错,如果你把程式码看更清楚一点,你会发现答案其实是C. 不会,在这情况下 Component 会比 PureComponent 有效率。

你的前提是对的,「只要 state 跟 props 没变,就不会 re-render,PureComponent 就会比 Component 更有效率」。但其实还有另外一句话也是对的:「如果你的 state 或 props 『永远都会变』,那 PureComponent 并不会比较快」。

所以这两种的使用时机差异在于:state 跟 props 到底常常会变还是不会变?

上述的例子中,陷阱在于itemStyle这个 props,我们每次 render 的时候都创建了一个新的物件,所以对 Row 来说,儘管 props.item 是一样的,但是 props.style 却是「每次都不一样」。

如果你已经知道每次都会不一样,那 PureComponent 这时候就无用武之地了,而且还更糟。为什么?因为它帮你做了shallowEqual。

别忘记了,shallowEqual也是需要执行时间的。

已经知道 props 的比较每次都失败的话,那不如不要比还会来的比较快,所以在这个情形下,Component 会比 PureComponent 有效率,因为不用做shallowEqual。

这就是我开头提到的需要特别注意的部分。不要以为你把每个 Component 都换成 PureComponent 就天下太平,App 变超快,效能提升好几倍。不去注意这些细节的话,就有可能把效能越弄越糟。

最后再强调一次,如果你已经预期到某个 component 的 props 或是 state 会「很频繁变动」,那你根本不用换成 PureComponent,因为你实作之后反而会变得更慢。

总结

在研究这些效能相关的问题时,我最推荐这篇:React, Inline Functions, and Performance,解开了很多我心中的疑惑以及带给我很多新的想法。

例如说文末提到的 PureComponent 有时候反而会变慢,也是从这篇文章看来的,真心推荐大家抽空去看看。

前阵子跟同事一起把一个专案打掉重做,原本的共识是尽量用 PureComponent,直到我看到这篇文并且仔细思考了一下,发现如果你不知道背后的原理,还是不要轻易使用比较好。因此我就提议改成全部用 Component,等我们碰到效能问题要来优化时再慢慢调整。

最后附上一句我很喜欢的话,从React 巢状 Component 效能优化这篇看来的(这篇也是在讲最后提到的 PureComponent 的问题):

虽然你知道可以优化,但不代表你应该优化。

原文发布时间为:2018-09-8

本文来自云栖社区合作伙伴“”,了解相关信息可以关注“”。

转载地址:http://hmtpo.baihongyu.com/

你可能感兴趣的文章
ASP.NET Aries 入门开发教程6:列表数据表格的格式化处理及行内编辑
查看>>
51CTO首届卡拉OK大赛:我唱,为欢聚而歌
查看>>
LVM逻辑卷管理详解
查看>>
如何实现 Service 伸缩?- 每天5分钟玩转 Docker 容器技术(97)
查看>>
java socket编程
查看>>
MySQL基础建设之硬盘篇
查看>>
Java格式化时间
查看>>
安装Hyper-V
查看>>
配置XenDesktop一例报错-序列不包含任何元素
查看>>
数组循环移位
查看>>
一个优秀的公众号运营者需要具备哪些能力?
查看>>
桌面云
查看>>
教大家如何在word 2007中同时打出对齐上下标以及字母头上有波浪线(非编辑器)...
查看>>
Spring Boot五:使用properties配置文件实现多环境配置
查看>>
vim取消高亮显示
查看>>
设计从“心“开始
查看>>
windows7 系统盘 瘦身软件介绍: 冗余文件清理工具
查看>>
SSH整合步骤
查看>>
myeclipse tomcat内存溢出解决方法
查看>>
zabbix之Web网络监控
查看>>