setState的性能优化化化化化

React更新

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。

  • React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
  • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量;这个开销太过昂贵了,React的更新性能会变得非常低效;

于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

  • 同层节点之间相互比较,不会垮节点比较;
  • 不同类型的节点,产生不同的树结构;
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

Diffing算法

1. 不同类型的元素

当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:

  • 当一个元素从 a变成 img,从 Article 变成 Comment,或从 Button 变成 div 都会触发一个完整的重建流程;
  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
  • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法;

如下:
React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用;

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>
2. 同一类型的元素

当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。

例如:

  • 当更新 style 属性时,React 仅更新有所更变的属性。
  • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。
<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

所以如果是同类型的组件元素:

组件会保持不变,React会更新该组件的props,并且调用
componentWillReceiveProps() 和 componentWillUpdate() 方法;

下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;

3.对子节点进行递归

在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。

我们来看一下在最后插入一条数据的情况:

  • 前面两个比较是完全相同的,所以不会产生mutation;
  • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

但是如果我们是在中间插入一条数据:

  • React会对每一个子元素产生一个mutation,而不是保持 星际穿越和盗梦空间的不变;
  • 这种低效的比较方式会带来一定的性能问题;
<ul>
  <li>星际穿越</li>
  <li>盗梦空间</li>
</ul>

<ul>
  <li>大话西游</li>
  <li>星际穿越</li>
  <li>盗梦空间</li>
</ul>

keys的优化

我们通过一个栗子进行讲解

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      movies: ["星际穿越", "盗梦空间"]
    }
  }

  render() {
    return (
      <div>
        <h2>电影列表</h2>
        <ul>
          {
            this.state.movies.map((item, index) => {
              return <li>{item}</li>
            })
          }
        </ul>
        <button onClick={e => this.insertMovie()}>插入数据</button>
      </div>
    )
  }

  insertMovie() {
  }
}

1.在最后位置插入数据(这种情况,有无key意义并不大)

insertMovie() {
  const newMovies = [...this.state.movies, "大话西游"];
  this.setState({
    movies: newMovies
  })
}

2.在前面插入数据(这种做法,在没有key的情况下,所有的li都需要进行修改;)

insertMovie() {
  const newMovies = ["大话西游", ...this.state.movies];
  this.setState({
    movies: newMovies
  })
}

当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

  • 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;
  • 将key为333的元素插入到最前面的位置即可;
<ul>
  <li key="111">星际穿越</li>
  <li key="222">盗梦空间</li>
</ul>

<ul>
  <li key="333">Connecticut</li>
  <li key="111">星际穿越</li>
  <li key="222">盗梦空间</li>
</ul>

key的注意事项:

key应该是唯一的;
key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
使用index作为key,对性能是没有优化的;

shouldComponentUpdate优化

通过如下案例进行介绍:

import React, { Component } from 'react';

function Header() {
  console.log("Header Render 被调用");
  return <h2>Header</h2>
}

class Main extends Component {
  render() {
    console.log("Main Render 被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

function Banner() {
  console.log("Banner Render 被调用");
  return <div>Banner</div>
}

function ProductList() {
  console.log("ProductList Render 被调用");
  return (
    <ul>
      <li>商品1</li>
      <li>商品2</li>
      <li>商品3</li>
      <li>商品4</li>
      <li>商品5</li>
    </ul>
  )
}

function Footer() {
  console.log("Footer Render 被调用");
  return <h2>Footer</h2>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    console.log("App Render 被调用");

    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
}

那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:

  • 事实上,很多的组件没有必须要重新render;
  • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法;

React给我们提供了一个生命周期方法shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:

该方法有两个参数:

  • 参数一:nextProps 修改之后,最新的props属性
  • 参数二:nextState 修改之后,最新的state属性

该方法返回值是一个boolean类型

  • 返回值为true,那么就需要调用render方法;
  • 返回值为false,那么久不需要调用render方法;
  • 默认返回的是true,也就是只要state发生改变,就会调用render方法;
shouldComponentUpdate(nextProps, nextState) {
  return true;
}

我们可以控制它返回的内容,来决定是否需要重新渲染。

比如我们在App中增加一个message属性:

  • jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染;
  • 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了;
export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
      message: "Hello World"
    }
  }

  render() {
    console.log("App Render 被调用");

    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.changeText()}>改变文本</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  changeText() {
    this.setState({
      message: "你好啊,李银河"
    })
  }
}

这个时候,我们可以通过实现shouldComponentUpdate来决定要不要重新调用render方法:

  • 这个时候,我们改变counter时,会重新渲染;
  • 如果,我们改变的是message,那么默认返回的是false,那么就不会重新渲染;
shouldComponentUpdate(nextProps, nextState) {
  if (nextState.counter !== this.state.counter) {
    return true;
  }

  return false;
}

但是,当counter改变时,所有的子组件依然重新渲染了:

我们应该实现所有的子组件的shouldComponentUpdate;

  • 比如Main组件,可以进行如下实现:
  • shouldComponentUpdate默认返回一个false;
    在特定情况下,需要更新时,我们在上面添加对应的条件即可;
class Main extends Component {

  shouldComponentUpdate(nextProps, nextState) {
    return false;
  }

  render() {
    console.log("Main Render 被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

PureComponent和memo

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
将class基础自PureComponent。
将上面的代码进行修改之后:

class Main extends PureComponent {
  render() {
    console.log("Main Render 被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

PureComponent的原理是什么呢?

对props和state进行浅层比较;

那么,如果是一个函数式组件呢?我们需要使用一个高阶组件memo:

  • 我们将之前的Header、Banner、ProductList都通过memo函数进行一层包裹;
  • Footer没有使用memo函数进行包裹;
  • 最终的效果是,当counter发生改变时,Header、Banner、ProductList的函数不会重新执行,而Footer的函数会被重新执行;
import React, { Component, PureComponent, memo } from 'react';

const MemoHeader = memo(function() {
  console.log("Header Render 被调用");
  return <h2>Header</h2>
})

class Main extends PureComponent {
  render() {
    console.log("Main Render 被调用");
    return (
      <div>
        <MemoBanner/>
        <MemoProductList/>
      </div>
    )
  }
}

const MemoBanner = memo(function() {
  console.log("Banner Render 被调用");
  return <div>Banner</div>
})

const MemoProductList = memo(function() {
  console.log("ProductList Render 被调用");
  return (
    <ul>
      <li>商品1</li>
      <li>商品2</li>
      <li>商品3</li>
      <li>商品4</li>
      <li>商品5</li>
    </ul>
  )
})

function Footer() {
  console.log("Footer Render 被调用");
  return <h2>Footer</h2>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0,
      message: "Hello World"
    }
  }

  render() {
    console.log("App Render 被调用");

    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.changeText()}>改变文本</button>
        <MemoHeader/>
        <Main/>
        <Footer/>
      </div>
    )
  }

  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (nextState.counter !== this.state.counter) {
      return true;
    }

    return false;
  }

  changeText() {
    this.setState({
      message: "你好啊,李银河"
    })
  }
}

memo的原理是什么呢?

  • 最终返回一个对象,这个对象中有一个compare函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值