React的更新机制以及性能优化

93 阅读6分钟

React的更新机制

众所周知,React的渲染流程是将JSX转换为虚拟DOM再转换为真实DOM。那React的更新流程呢? 如图: image.png

render函数重新执行后,产生新的DOM树,而React会对新旧的DOM树计算差异,者之间用的就是diff算法。计算后会打补丁patch,更新到真实的DOM。

React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI。如果一棵树参考另外一棵树进行完全比较更新(递归),那么即使是最先进的算法 该算法的复杂程度为 O(n^3 ),其中n是树中元素的数量。如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销太过昂贵了,React的更新性能会变得非常低效。

所以React对这个算法进行了优化,将其复杂度降为了O(n),如何进行优化的呢?

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

情况一:对比不同类型的元素

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

比如下面的代码更改。

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

image.png

情况二:对比同一类型的元素

  • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性
  • 比如下面的代码更改
    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性 image.png
  • 比如下面的代码更改
    • 当更新 style 属性时,React 仅更新有所更变的属性
    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight

image.png

  • 如果是同类型的组件元素
    • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps() 和 componentWillUpdate() 方法
    • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归

情况三:对子节点进行递归

  • 在默认条件下,当递归 DOM 节点的子元素时,React 会同 时遍历两个子元素的列表;当产生差异时,生成一个mutation(变化)。
    • 我们来看一下在最后插入一条数据的情况 image.png

    • 前面两个比较是完全相同的,所以不会产生mutation,最后一个比较,产生一个mutation,将其插入到新的 DOM树中即可。

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

    • React会对每一个子元素产生一个mutation,而不是保持 < li >星际穿越< /li >和

    • 盗梦空间< /li >的不变

这种低效的比较方式会带来一定的性能问题

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() {
    console.log('添加电影');
  }
}

页面渲染出来了,我们会发现报这么一个warning。建议我们列表最好加一个唯一的key

image.png

image.png

image.png

所以keys就很好使(特别是在插入数据的时候)

image.png

当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。 有了keys以后,React就会对原来的数组内容向后做位移的操作。这样大大的提高了性能。

keys要注意:

  • key应该是唯一的
  • key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
  • 使用index(遍历的索引)作为key,对性能是没有优化的(因为我们在最前面插入数据,原来的数据比如index是0和1,我后面的插入上来到最前面它变成了1和2,这还是没匹配上,匹配不了还是会创建index)

看一个组件嵌套的函数调用

image.png

import React, { Component } from 'react';

// Header
function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
}

// Main
class Banner extends Component {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

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

class Main extends Component {
  render() {
    console.log("Main render函数被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}
// Footer
function Footer() {
  console.log("Footer被调用");
  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
    })
  }
}

它们创建的时候会被调用。 image.png

页面刷新展示的结果。 image.png

当我们点击+号跟下面的组件没半毛钱关系,我们不希望它们被重新render image.png 但是我们点击一次,所有的东西都被重新render了。 image.png 这个太浪费性能了。我们需要做优化,这样我们就得用到shouldComponentUpdate

使用shouldComponentUpdate

import React, {Component} from 'react';

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>
      </div>
    )
  }

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

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

  changeText() {
    this.setState({
      message: "张三你好"
    })
  }
}

image.png 如果原来的counter不能与最新的couter就return true。 这里shouldComponentUpdate中有两个参数(nextProps,nextState)

开发中我们肯定希望使用这个优化,但是它是类所特有的,而且写太多,都手动写,工作量太大了。

其实我们只需要让这个类继承于PureComponent(这个类会自动实现这shouldComponentUpdate)

PureComponent

import React, { PureComponent } from 'react';

// Header
function Header() {
  console.log("Header被调用");
  return <h2>我是Header组件</h2>
}

// Main
class Banner extends PureComponent {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

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

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

// Footer
function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
}


export default class App extends PureComponent {
  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
    })
  }
}

shouldComponentUpdate()return true和 return false决定组件更不更新的源码:

image.png

image.png PureComponent的源码

image.png

image.png

浅层比较所在的位置, image.png

浅层比较会比较这两个东西是否相同 image.png

结论: image.png

此外:在开发中只要做浅层比较就行了,最好不要做深层比较,那个会十分消耗性能。(官网有说) image.png

但是PureComponent做不到一个事情。它解决不了函数式组件重复调用的问题。我们得用到memo。

memo的使用

import React, { PureComponent, memo } from 'react';

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


// Main
class Banner extends PureComponent {
  render() {
    console.log("Banner render函数被调用");
    return <h3>我是Banner组件</h3>
  }
}

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

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

// Footer
const MemoFooter = memo(function Footer() {
  console.log("Footer被调用");
  return <h2>我是Footer组件</h2>
})


export default class App extends PureComponent {
  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>
        <MemoHeader/>
        <Main/>
        <MemoFooter/>
      </div>
    )
  }

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

image.png

image.png