www.2003.comReact 性能优化大挑战:一次理解 Immuta

2019-05-02 作者:计算机教程   |   浏览(157)

React 性能优化大挑战:一次理解 Immutable data 跟 shouldComponentUpdate

2018/01/08 · JavaScript · ReactJS

原文出处: TechBridge Weekly/huli   

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

  1. Component 跟 PureComponent 的差异
  2. shouldComponentUpdate 的作用
  3. React 的渲染机制
  4. 为什麽要用 Immutable data structures

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

一、优化原理

改写react生命周期shouldComponentUpdate,使其在需要重新渲染当前组件时返回true,否则返回false。不再全部返回true。

React 小测验

二、主流优化方式

第一题

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

JavaScript

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

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
class Content extends React.Component {
  render () {
    console.log('render content!');
    return <div>Content</div>
  }
}
  
class App extends React.Component {
  handleClick = () => {
    this.setState({
      a: 1
    })
  }
  render() {
    console.log('render App!');
    return (
      <div>
        <button onClick={this.handleClick}>setState</button>
        <Content />
      </div>
    );
  }
}
  
ReactDOM.render(
  <App />,
  document.getElementById('container')
);

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

A. 什麽都没有(App 跟 Content 的 render function 都没被执行到)
B. 只有 render App!(只有 App 的 render function 被执行到)
C. render App! 以及 render content!(两者的 render function 都被执行到)

1.react官方解决方案

原理:重写默认的shouldComponentUpdate,将旧props、state与新props、state逐个进行浅比较(形如:this.props.option === nextProps.option ?  false : true),如果全部相同,返回false,如果有不同,返回true。

PureRenderMixin(es5):

var PureRenderMixin = require('react-addons-pure-render-mixin');

    React.createClass({

    mixins: [PureRenderMixin],

    render: function() {

         return <div className={this.props.className}>foo</div>;

    }

});

Shallow Compare (es6):

var shallowCompare = require('react-addons-shallow-compare');

export class SampleComponent extends React.Component {

    shouldComponentUpdate(nextProps, nextState) {

        return shallowCompare(this, nextProps, nextState);

    }

    render() {

        return <div className={this.props.className}>foo</div>
    }

}

es7装饰器的写法:

import pureRender from "pure-render-decorator"

...

@pureRender

class SampleComponent extends Component {

    render() {

        return (

            <div className={this.props.className}>foo</div>

        )

    }

}

react 15.3.0 写法(用来替换react-addons-pure-render-mixin):

class SampleComponent extends React.PureComponent{

    render(){

        return(

            <div className={this.props.className}>foo</div>

        )

    }

}

第二题

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

JavaScript

class Row extends Component { render () { const {item, style} = this.props; return ( <tr style={style}> <td>{item.id}</td> </tr> ) } } class Table extends Component { render() { const {list} = this.props; const itemStyle = { color: 'red' } return ( <table> {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)} </table> ) } } 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 ( <div> <button onClick={this.handleClick}>change state!</button> <Table list={list} /> </div> ); } }

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
37
38
39
40
41
42
43
44
45
46
class Row extends Component {
  render () {
    const {item, style} = this.props;
    return (
      <tr style={style}>
        <td>{item.id}</td>
      </tr>
    )
  }
}
  
class Table extends Component {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: 'red'
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}
  
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 (
      <div>
        <button onClick={this.handleClick}>change state!</button>
        <Table list={list} />
      </div>
    );
  }
}

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

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Table extends PureComponent {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: 'red'
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}
  
// 不知道什麽是 PureComponent 的朋友,可以想成他自己帮你加了下面的 function
shouldComponentUpdate (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 有效率

*上述方案存在问题(浅比较的问题):

(1)某些props、state值未改变的情况,返回true,例如:

 <Cell options={this.props.options || [ ]} />

当this.props.options == false时,options=[ ]。当父组件两次渲染,this.props.options一直 == false,对于Cell组件来说,options没有改变,不需要重新渲染。但Cell的shouldComponentUpdate中进行的是浅比较,由于[ ] !== [ ],所以,this.props.options === nextProps.options为false,shouldComponentUpdate会返回true,Cell将进行重新渲染。

解决方法如下:

const default = [ ];

<Cell options={this.props.options || default} />

(2)某些props、state值改变的情况,返回false,例如:

handleClick() {

    let {items} = this.state;

    items.push('new-item') ;

    this.setState({ items });

}

render() {

    return (

        <div>

            <button onClick={this.handleClick} />

            <ItemList items={this.state.items} />

        </div>
    )

}

如果ItemList是纯组件(PureComponent),那么这时它是不会被渲染的。因为尽管this.state.items的值发生了改变,但是它仍然指向同一个对象的引用。

第三题

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

JavaScript

class Row extends Component { render () { const {item, style} = this.props; return ( <tr style={style}> <td>{item.id}</td> </tr> ) } } class Table extends PureComponent { render() { const {list} = this.props; const itemStyle = { color: 'red' } return ( <table> {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)} </table> ) } } 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 ( <div> <button onClick={this.handleClick}>change state!</button> <Table list={list} /> </div> ); } }

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
37
38
39
40
41
42
43
44
45
46
class Row extends Component {
  render () {
    const {item, style} = this.props;
    return (
      <tr style={style}>
        <td>{item.id}</td>
      </tr>
    )
  }
}
  
class Table extends PureComponent {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: 'red'
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}
  
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 (
      <div>
        <button onClick={this.handleClick}>change state!</button>
        <Table list={list} />
      </div>
    );
  }
}

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Row extends PureComponent {
  render () {
    const {item, style} = this.props;
    return (
      <tr style={style}>
        <td>{item.id}</td>
      </tr>
    )
  }
}
  
class Table extends PureComponent {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: 'red'
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}

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

A. 会,在这情况下 PureComponent 会比 Component 有效率
B. 不会,两者差不多
C. 不会,在这情况下 Component 会比 PureComponent 有效率

2.Immutable

原理:Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。

Immutable Data就是一旦被创建,就不能再更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。同时,为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

使用举例:

import { is } from 'immutable';

for (const key in nextProps) {

    if (!is(thisProps[key], nextProps[key])) {

        return true;

    }

}

immutable.js框架是非常好的Immutable库,其他可用api,详见官方文档。

使用原则:

由于侵入性较强,新项目引入比较容易,老项目迁移需要谨慎评估迁移成本。对于一些提供给外部使用的公共组件,最好不要把Immutable对象直接暴露在对外的接口中。

React 的 render 机制

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

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

JavaScript

class Content extends React.Component { render () { return <div>Content</div> } }

1
2
3
4
5
class Content extends React.Component {
  render () {
    return <div>Content</div>
  }
}

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

JavaScript

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

1
2
3
4
{
  tagName: 'div',
  children: 'Content'
}

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

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

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

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Content extends React.Component {
  render () {
    return <div>{this.props.text}</div>
  }
}
  
class App extends React.Component {
  state = {
    text: 'hello'
  }
  handleClick = () => {
    this.setState({
      text: 'world'
    })
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>setState</button>
        <Content text={this.state.text} />
      </div>
    );
  }
}

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

  1. 呼叫 App 的 render
  2. 呼叫 Content 的 render
  3. 拿到 virtual DOM
  4. 跟上次的 virtual DOM 做比较
  5. 把改变的地方应用到真的 DOM

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
{
  tagName: 'div',
  children: [
    {
      tagName: 'button',
      children: 'setState'
    }, {
      tagName: 'div',
      children: 'hello'
    }
  ]
}

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

  1. 呼叫 App 的 render
  2. 呼叫 Content 的 render
  3. 拿到 virtual DOM

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
{
  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 上面去。

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

  1. render function
  2. virtual DOM diff

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

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

我们先从后者开始吧!

本文由www.2003.com发布于计算机教程,转载请注明出处:www.2003.comReact 性能优化大挑战:一次理解 Immuta

关键词: