likes
comments
collection
share

前端小白学 React 框架(十一)

作者站长头像
站长
· 阅读数 19

React性能优化

React的更新流程

React在props或state发生改变时,会调用React的render方法,会创建一棵不同的树。React需要基于这两棵不同的树之间的差别来判断如何有效的更新UI,如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度都会非常高,具体可参考这篇论文,如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围,这个开销太过昂贵了,React的更新性能会变得非常低效。于是,React对这个算法进行了优化,将其优化成了O(n):

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

keys的优化

一般情况下不写key的话会在控制台看到这样的报错:

前端小白学 React 框架(十一)

  1. 在最后位置插入数据,这种情况,有无key意义并不大
  2. 在前面插入数据,这种做法,在没有key的情况下,所有的li都需要进行修改
  3. key的注意事项
    • key应该是唯一的
    • key不要使用随机数 (随机数在下一次render时,会重新生成一个数字)
    • 使用index作为key,对性能是没有优化的;

SCU优化

看个小例子🌰,有如下代码:

import React, { Component } from 'react';
import Home from './Home.jsx';
import Recommand from './Recommand.jsx';

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

    this.state = {
      message: 'hello world',
      count: 0
    }
  }

  changeText() {
    this.setState({ message: 'hello react' });
  }

  render() {
    console.log('App render');
    const { message, count } = this.state;
    return (
      <div>
        <h1>render函数的优化</h1>
        <h2>App-{message}-{count}</h2>
        <button onClick={() => this.changeText()}>修改文本</button>
        <Home />
        <Recommand />
      </div>
    )
  }
}

export default App

点击按钮后,文本会更改,但是HomeRecommand组件没有更改,因此我们想着应该不应该调用render函数,但是事实真实如此嘛?看一下实际的案例:

前端小白学 React 框架(十一)

可以发现,HomeRecommand组件的render函数还是被调用了,这样就造成了性能的浪费。甚至运行this.setState({ message: 'hello world' });也会重新调用render函数。

解决方法

那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法性能必然是很低的,事实上,很多的组件没有必须要重新render;它们调用render应该有一个前提,就是依赖的数据 (state、props)发生改变时,再调用自己的render方法

那么就可以通过shouldComponentUpdate方法来控制render方法是否被调用

shouldComponentUpdate

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

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

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

  • 返回值为true,那么就需要调用render方法;
  • 返回值为false,那么就不需要调用render方法;
  • 默认返回的是true,也就是只要state发生改变,就会调用render方法。

比如我们在App中增加一个message属性,jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染,但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了。

具体的使用看如下例子:

import React, { Component } from 'react'

export class Home extends Component {
  shouldComponentUpdate() {
    return false;
  }

  render() {
    console.log('Home render');
    return (
      <div>Home</div>
    )
  }
}

export default Home

但是直接return false会有一个缺点,就是如果真的有数据要更改,那么也不会发生改变,因此需要自己写判断逻辑控制组件是否调用render函数。

那么我们现在实现一个需求:如果更改的内容与以前不一样就调用render,否则不调用,来看个例子:

import React, { Component } from 'react';
import Home from './Home.jsx';
import Recommand from './Recommand.jsx';

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

    this.state = {
      message: 'hello world',
      count: 0
    }
  }

  changeText() {
    this.setState({ message: 'hello world' });
  }

  changeText2() {
    this.setState({ message: 'hello react' });
  }

  shouldComponentUpdate(newProps, newState) {
    return this.state.message !== newState.message;
  }

  render() {
    console.log('App render');
    const { message, count } = this.state;
    return (
      <div>
        <h1>render函数的优化</h1>
        <h2>App-{message}-{count}</h2>
        <button onClick={() => this.changeText()}>修改相同的文本</button>
        <button onClick={() => this.changeText2()}>修改不同的文本</button>
        <Home />
        <Recommand />
      </div>
    )
  }
}

export default App

以下是运行结果:

前端小白学 React 框架(十一)

💡 子组件不仅要对比自己的state还要对比传过来的props

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量,我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false。

🎉 好消息!!!

🎉 好消息!!!

🎉 好消息!!!

事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?将class继承自PureComponent

PureComponent

示例代码如下:

import React, { PureComponent } from 'react';
// ...

export class App extends PureComponent {
  // ...

  // shouldComponentUpdate(newProps, newState) {
  //   return this.state.message !== newState.message;
  // }

  render() {
    // ...
  }
}

大家可以去控制台看打印结果,发现可以实现上面SCU优化的效果,但是PureComponent只能进行浅层比较

那么如果是函数式组件也想要实现这样的功能呢?React也提供了memo函数。

memo

export default function Profile(props) {
  console.log('Profile render');
  return <h2>Profile: {props.message}</h2>
}

如果点击+1按钮,但是Profile组件没有依赖count,但是也会发生更改,如果没有加memo的效果是这样的:

前端小白学 React 框架(十一)

因此为了解决这个问题,就需要用到memo了:

import { memo } from "react";

const Profile = memo(function (props) {
  console.log('Profile render');
  return <h2>Profile: {props.message}</h2>
});

export default Profile;

memo的效果是这样的:

前端小白学 React 框架(十一)

不可变数据的力量

看下面的例子:

import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      books: [
        { name: 'book1', price: 90, count: 1 },
        { name: 'book2', price: 49, count: 2 },
        { name: 'book3', price: 59, count: 4 },
        { name: 'book4', price: 99, count: 1 },
      ]
    }
  }

  addBook() {
    const newBook = { name: 'book5', price: 19, count: 6 };
    this.state.books.push(newBook);
    this.setState({
      books: this.state.books
    })
  }

  render() {
    return (
      <div>
        <h1>数据不可变的力量</h1>
        <ul>
          {
            this.state.books.map((book, index) => {
              return (
                <li key={index}>
                  <span>{book.name}-{book.price}-{book.count}</span>
                  <button>+1</button>
                </li>
              )
            })
          }
        </ul>
        <button onClick={() => this.addBook()}>添加新书</button>
      </div>
    )
  }
}

export default App

点击添加书籍按钮会发现页面不会发生变化,这是因为我们继承的是PureComponent每次进行setState操作的时候会对比两个变量是否发生改变,而对象存储的是内存地址,只使用push操作是不会重新将state里的变量赋值为新地址,因此就不会发生改变。

如何解决这个问题呢?只要将原数组浅拷贝一份就可以了,这样子页面就能更新了:

addBook() {
  const newBook = { name: 'book5', price: 19, count: 6 };
  const books = [...this.state.books, newBook];
  this.setState({ books });
}

效果如下:

前端小白学 React 框架(十一)

其他详情可以查看🔎官方文档Optimizing Performance

PureComponent更新操作的源码

function checkShouldComponentUpdate(
  workInProgress: Fiber,
  ctor: any,
  oldProps: any,
  newProps: any,
  oldState: any,
  newState: any,
  nextContext: any,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    let shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    if (__DEV__) {
      if (
        debugRenderPhaseSideEffectsForStrictMode &&
        workInProgress.mode & StrictLegacyMode
      ) {
        setIsStrictModeForDevtools(true);
        try {
          // Invoke the function an extra time to help detect side-effects.
          shouldUpdate = instance.shouldComponentUpdate(
            newProps,
            newState,
            nextContext,
          );
        } finally {
          setIsStrictModeForDevtools(false);
        }
      }
      if (shouldUpdate === undefined) {
        console.error(
          '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
            'boolean value. Make sure to return true or false.',
          getComponentNameFromType(ctor) || 'Component',
        );
      }
    }

    return shouldUpdate;
  }

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

  return true;
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      // $FlowFixMe[incompatible-use] lost refinement of `objB`
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

可以看出来,如果PureComponent中写了shouldComponentUpdate函数还是会执行的,并且优先级比源码的shallowEqual,但是一般PureComponent中不写shouldComponentUpdate

写在最后

如果大家喜欢的话可以收藏本专栏,之后会慢慢更新,然后大家觉得不错可以点个赞或收藏一下 🌟。

博客内的项目源码在react-app分支,大家可以拷贝下来。