59

图片描述

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

这是 Web 性能优化的第四篇,之前的可以在下面点击查看:

  1. Web 性能优化: 使用 Webpack 分离数据的正确方法
  2. Web 性能优化: 图片优化让网站大小减少 62%
  3. Web 性能优化: 缓存 React 事件来提高性能

React.js 核心团队一直在努力使 React 变得更快,就像燃烧的速度一样。为了让开发者能够加速他们的 React 应用程序,为此增加了很多工具:

  • Suspense 和 Lazy Load (React.lazy(…), <Suspense/>)
  • 纯组件
  • shouldComponentUpdate(…){…} 生命周期钩子

在这篇文章中,我们将介绍 React v16.6 中新增的另一个优化技巧,以帮助加速我们的函数组件:React.memo

提示:使用 Bit 共享和安装 React 组件。使用你的组件来构建新的应用程序,并与你的团队共享它们以更快地构建。

图片描述

浪费的渲染

组件构成 React 中的一个视图单元。这些组件具有状态,此状态是组件的本地状态,当状态值因用户操作而更改时,组件知道何时重新渲染。现在,React 组件可以重新渲染 5、10 到 90次。有时这些重新渲染可能是必要的,但大多数情况下不是必需的,所以这些不必要的这将导致我们的应用程序严重减速,降低了性能。

来看看以下这个组件:

import React from 'react'

class TestC extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('componentWillUpdate')
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate')
  }

  render () {
    return (
      <div>
        {this.state.count}
        <button onClick={ () => this.setState({count: 1})}>Click Me</button>
      </div>
    )
  }
}

export default TestC;

该组件的初始状态为 {count: 0} 。当我们单击 click Me 按钮时,它将 count 状态设置为 1。屏幕的 0 就变成了 1。.当我们再次单击该按钮时出现了问题,组件不应该重新呈现,因为状态没有更改。count 的上个值为1,新值也 1,因此不需要更新 DOM。

这里添加了两个生命周期方法来检测当我们两次设置相同的状态时组件 TestC 是否会更新。我添加了componentWillUpdate,当一个组件由于状态变化而确定要更新/重新渲染时,React 会调用这个方法;还添加了componentdidUpdate,当一个组件成功重新渲染时,React 会调用这个方法。

在浏览器中运行我们的程序,并多次单击 Click Me 按钮,会看到在控制打印很多次信息:

图片描述

在我们的控制台中有 “componentWillUpdate” 和 “componentWillUpdate” 日志,这表明即使状态相同,我们的组件也在重新呈现,这称为浪费渲染。

纯组件/shouldComponentUpdate

为了避免 React 组件中的渲染浪费,我们将挂钩到 shouldComponentUpdate 生命周期方法。

shouldComponentUpdate 方法是一个生命周期方法,当 React 渲染 一个组件时,这个方法不会被调用 ,并根据返回值来判断是否要继续渲染组件。

假如有以下的 shouldComponentUpdadte:

shouldComponentUpdate (nextProps, nextState) {
  return true
}
  • nextProps: 组件接收的下一个 props 值。
  • nextState: 组件接收的下一个 state 值。

在上面,告诉 React 要渲染我们的组件,这是因为它返回 true

如果我们这样写:

shouldComponentUpdate(nextProps, nextState) {
   return false
}

我们告诉 React 永远不要渲染组件,这是因为它返回 false

因此,无论何时想要渲染组件,都必须返回 true。现在,可以重写 TestC 组件:

import React from 'react';
class TestC extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
    }
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (this.state.count === nextState.count) {
            return false
        }
        return true
    }
    render() {
        return ( 
            <div> 
            { this.state.count } 
            <button onClick = {
                () => this.setState({ count: 1 }) }> Click Me </button> 
            </div>
        );
    }
}
export default TestC;

我们在 TestC 组件中添加了 shouldComponentUpdate,我们检查了当前状态对象this.state.count 中的计数值是否等于 === 到下一个状态 nextState.count 对象的计数值。 如果它们相等,则不应该重新渲染,因此我们返回 false,如果它们不相等则返回 true,因此应该重新渲染以显示新值。

在我们的浏览器中测试,我们看到我们的初始渲染:

图片描述

如果我们多次点击 click Me 按钮,我们只会得到:

componentWillUpdate
componentDidUpdate 

图片描述

我们可以从 React DevTools 选项卡中操作 TestC 组件的状态,单击 React 选项,选择右侧的 TestC,我们将看到带有值的计数状态:

图片描述

在这里,我们可以改变数值,点击count文本,输入 2,然后回车:

图片描述

你会看到状态计数增加到 2,在控制台会看到:

componentWillUpdate
componentDidUpdate
componentWillUpdate
componentDidUpdate

图片描述

这是因为上个值为 1 且新值为 2,因此需要重新渲染。

现在,使用 纯组件

React在v15.5中引入了Pure Components。 这启用了默认的相等性检查(更改检测)。 我们不必将 shouldComponentUpdate 生命周期方法添加到我们的组件以进行更改检测,我们只需要继承 React.PureComponent,React 将会自己判断是否需要重新渲染。

注意:

1)继承 React.PureComponent 时,不能再重写 shouldComponentUpdate,否则会引发警告

Warning: ListOfWords has a method called shouldComponentUpdate(). shouldComponentUpdate should not be used when extending React.PureComponent. Please extend React.Component if shouldComponentUpdate is used.

2)继承PureComponent时,进行的是浅比较,也就是说,如果是引用类型的数据,只会比较是不是同一个地址,而不会比较具体这个地址存的数据是否完全一致。

class ListOfWords extends React.PureComponent {
 render() {
     return <div>{this.props.words.join(',')}</div>;
 }
}
class WordAdder extends React.Component {
 constructor(props) {
     super(props);
     this.state = {
         words: ['marklar']
     };
     this.handleClick = this.handleClick.bind(this);
 }
 handleClick() {
     // This section is bad style and causes a bug
     const words = this.state.words;
     words.push('marklar');
     this.setState({words: words});
 }
 render() {
     return (
         <div>
             <button onClick={this.handleClick}>click</button>
             <ListOfWords words={this.state.words} />
         </div>
     );
 }
}

上面代码中,无论你怎么点击按钮,ListOfWords 渲染的结果始终没变化,原因就是WordAdderword 的引用地址始终是同一个。

当然如果想让你变化,只要在 WordAdder 的 handleClick 内部,将 const words = this.state.words; 改为 const words = this.state.words.slice(0),就行了,因为改变了引用地址。

3)浅比较会忽略属性或状态突变的情况,其实也就是,数据引用指针没变而数据被改变的时候,也不新渲染组件。但其实很大程度上,我们是希望重新渲染的。所以,这就需要开发者自己保证避免数据突变。

接着让我们修改我们的 TestC 组件来使用 PureComponent:

import React from 'react';
class TestC extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate')
    }
    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate')
    }
    /*shouldComponentUpdate(nextProps, nextState) {
        if (this.state.count === nextState.count) {
            return false
        }
        return true
    }*/
    render() {
        return ( 
            <div> 
            { this.state.count } 
            <button onClick = {
                () => this.setState({ count: 1 })
            }> Click Me </button> 
            </div >
        );
    }
}
export default TestC;

这里注释掉了 shouldComponentUpdate 实现,我们不需要它,因为 React.PureComponent 将为我们做检查。

试它,重新加载你的浏览器,并点击多次点击 Click Me 按钮:

图片描述

图片描述

现在,我们已经看到如何在 React 中优化类组件中的重新渲染,让我们看看我们如何在函数组件中实现同样的效果。

函数组件

现在,我们看到了如何使用 Pure Components 和 shouldComponentUpdate 生命周期方法优化上面的类组件,是的,类组件是 React 的主要组成部分。 但是函数也可以在 React 中用作为组件。

function TestC(props) {
  return (
    <div>
      I am a functional component
    </div>
  )
}

这里的问题是,函数组件没有像类组件有状态(尽管它们现在利用Hooks useState的出现使用状态),而且我们不能控制函数组件的是否重新渲染,因为我们不能像在类组件中使用生命周期方法。

如果可以将生命周期钩子添加到函数组件,那么就以添加 shouldComponentUpdate 方法来告诉React 什么时候重新渲染组件。 当然,在函数组件中,我们不能使用 extend React.PureComponent 来优化我们的代码

让我们将 TestC 类组件转换为函数组件。

import Readct from 'react';

const TestC = (props) => {
  console.log(`Rendering TestC :` props)
  return (
    <div>
      {props.count}
    </div>
  )
}

export default TestC;

// App.js
<TextC count={5}/>

在第一次渲染时,它将打印 Rendering TestC :5

图片描述

打开 DevTools 并单击 React 选项。在这里,更改 TestC 组件的 count5.

图片描述

如果我们更改数字并按回车,组件的 props 将更改为我们在文本框中输入的值,接着继续更为 45:

图片描述

移动到 Console 选项

图片描述

我们看到 TestC 组件重新渲染,因为上个值为 5,当前值为 45.现在,返回 React 选项并将值更改为 45,然后移至 Console:

图片描述

看到组件重新渲染,且上个值与当前值是一样的。

我们如何控制重新渲染?

解决方案:使用 React.memo()

React.memo(...) 是 React v16.6 中引入的新功能。 它与 React.PureComponent 类似,它有助于控制 函数组件 的重新渲染。 React.memo(...) 对应的是函数组件,React.PureComponent 对应的是类组件。

如何使用 React.memo(…)

这很简单,假设有一个函数组件,如下:

const Funcomponent = ()=> {
    return (
        <div>
            Hiya!! I am a Funtional component
        </div>
    )
}

我们只需将 FuncComponent 作为参数传递给 React.memo 函数:

const Funcomponent = ()=> {
    return (
        <div>
            Hiya!! I am a Funtional component
        </div>
    )
}
const MemodFuncComponent = React.memo(Funcomponent)

React.memo 会返回了一个纯组件 MemodFuncComponent。 我们将在 JSX 标记中渲染此组件。 每当组件中的 propsstate 发生变化时,React 将检查 上一个 stateprops 以及下一个 propsstate 是否相等,如果不相等则函数组件将重新渲染,如果它们相等则函数组件将不会重新渲染。

现在来试试 TestC 函数组件:

let TestC = (props) => {
    console.log('Rendering TestC :', props)
    return ( 
        <div>
        { props.count }
        </>
    )
}
TestC = React.memo(TestC);

打开浏览器并加载应用程序,打开 DevTools 并单击 React 选项,选择 <Memo(TestC)>

现在,如果我们在右边编辑 count 值为到 89,会看到我们的应用程序重新渲染:

图片描述

如果我们在将值改为与上个一样的值: 89:

图片描述

不会有重新渲染!!

总结

总结几个要点:

  • React.PureComponent 是银
  • React.memo(…) 是金。
  • React.PureComponent 是 ES6 类的组件
  • React.memo(...) 是函数组件
  • React.PureComponent 优化 ES6 类组件中的重新渲染
  • React.memo(...) 优化函数组件中的重新渲染

原文:

https://blog.bitsrc.io/improv...

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png


王大冶
68.1k 声望105k 粉丝