关于React的性能优化

362 阅读6分钟

一、React的diff算法

1.1 React更新机制

首先看一下Raect的渲染流程:

再看一下React的更新流程:

1.2 React更新流程

React在state或props发生改变时,调用render函数,渲染一颗新的虚拟DOM树;

React需要基于这两颗不同的树之间的差异来判别如何有效的更新UI:

  • 同层之间相互比较,不会跨节点比较;
  • 不同类型的节点,产生不同的树结构(如果以div标签为根的树改为了p标签,那div的所有子元素都重新渲染,产生p为根的树);
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

如图所示,只会进行同层之间的比较

tip:为什么不建议使用index作为key?

在列表渲染时,使用index作为key是没有性能提升的,因为diff算法在对key进行比较时,如果key为index索引,那么在列表中间插入一个值时,后面的所有index都会发生改变,原来元素的index和新的元素的index自然就会不同,即使他们是同一个元素,但是key为index,index不一致就会进行重渲染而不是移动原来的元素到新的位置。

二、SCU

在render函数执行时存在这么一个问题:

每当调用setState进行数据更新时,就会执行render函数来重新渲染我们的DOM结构,如果我们只更新了父组件中的数据,但是子组件内依赖的props并没有发生改变,只要调用render,就都会重新渲染,即使子组件的数据没有发生改变,这就会导致性能低下。

shouldComponentUpdate生命周期

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

该方法有两个参数:

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

这个生命周期函数可以决定render函数是否执行,返回值是一个boolean类型:

  • 如果它返回的是一个true,就执行render;
  • 如果返回的是false,那render不会执行,就不会重新渲染DOM。
  • 默认返回的是true,也就是说只要state发生改变,就会调用render函数来重新渲染DOM

PrueComponent

但是如果每次都在组件内部使用 shouldComponentUpdate, 那么开发起来真的是好麻烦啊;

  • 而使用shouldComponentUpdate的目的不外乎是当state中的数据发生改变时,来决定 shouldComponentUpdate 返回的是true还是false,来决定组件要不要重新渲染。
  • React已经提供了一个方法实现这点,只需要将class组件继承自 PureComponent类就好了。

demo如下:App组件有Home和Banner两个子组件,Home有App传递的props,Banner没有porps

import React, { PureComponent } from 'react'
import Home from './Home'
import Banner from './Banner'

export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      message: "Hello World"
    }
  }

  chanegText() {
    this.setState({
      message: "你好,世界"
    })
  }

  render() {
    console.log("App render")
    const { message } = this.state
    return (
      <div>
        <h2>{message}</h2>
        <button onClick={e => this.chanegText()}>修改文本</button>
        <Home message={message} />
        <Banner />
      </div>
    )
  }
}

export default App
import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  constructor(props) {
    super(props)
  }
  render() {
    console.log("Home render")
    const { message } = this.props
    return (
      <div>
        <h2>Home</h2>
        <div>{message}</div>
      </div>
    )
  }
}

export default Home
import React, { PureComponent } from 'react'

export class Banner extends PureComponent {
  constructor() {
    super()
  }
  render() {
    console.log("Banner render")
    return (
      <div>Banner</div>
    )
  }
}

export default Banner

memo 高阶组件

函数式组件如何实现PrueComponent那样在props没有改变时,不要重新渲染DOM树呢?;

只需要使用高阶组件memo就可以了:

import { memo } from "react"

const PreFile  = memo(function() {
  return (
    <div>
      PreFile
    </div>
  )
})

export default PreFile

React官网:zh-hans.react.dev/reference/r…

三、useSelector的第二个参数shallEqual

useSelector 的作用是将state映射到组件中:

  • 参数一:将state映射到需要的数据中;
  • 参数二:shallEqual, 从react-redux包中导入,可以进行比较来决定是否组件重新渲染;
import { connect } from 'react-redux';
import { shallowEqual } from 'react-redux';

const MyComponent = ({ data }) => {
  // 组件内部逻辑
};

const mapStateToProps = (state) => ({
  data: state.data,
});

export default connect(mapStateToProps, null, null, { areStatePropsEqual: shallowEqual })(MyComponent);

四、useCallback,useMemo,useRef

useCallback

  • 当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化之后的函数,传递给子组件;

如何进行性能的优化呢?

  • useCallback会返回一个函数的 memoized(记忆的)值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo, useCallback, useState } from 'react'

// Message组件
const Message = memo((props) => {
  const { addCount } = props
  console.log('Message组件被渲染')
  return (
    <div>
      <button onClick={addCount}>数字增加</button>
    </div>
  )
})

// App组件
const App = memo(() => {
  const [message, setMessage] = useState('Hello World')
  const [count, setCount] = useState(10)

  // 使用useCallback优化的函数:数字 + 1
  const addCount = useCallback(() => {
    setCount(count + 1)
  },[count])

  // 普通函数:数字 + 1
  // function addCount() {
  //   setCount(count + 1 )
  // }

  // 普通函数:修改文本
  function changeMessage() {
    setMessage(Math.random())
  }

  return (
    <div>
      <h2>App</h2>
      <div>{count}</div>
      <button onClick={addCount}>+1</button>
      <div>{message}</div>

      {/* 点击修改文本按钮时子组件不会重新渲染,
          因为传递给子组件的函数是通过useCallback优化过的,
          如果使用的是普通函数,则会重新渲染子组件 */}
      <button onClick={changeMessage}>修改文本</button>
      
      {/* 使用Message组件, 传递addCount方法 */}
      <Message addCount={addCount} />
    </div>
  )
})

export default App

可以看到使用了useCallback优化之后的addCount函数传递给子组件,在message发生改变时并没有重新渲染子组件,但是如果传递给子组件的是一个普通的函数,那么在message改变的时候也会重新渲染子组件

但是在 addCount 函数触发时也会让子组件重新渲染,那如果想要子组件在addCount函数触发时也不要重新渲染应该怎么做呢?

答案:使用 useRef,将addCount 方法进一步优化

下面介绍学习一下 useRef


useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

最常用的ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素;
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
import React, { memo } from 'react'
import { useRef } from 'react'

const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()

  function getTitleDom() {
    console.log(titleRef.current)
  }

  function getFocus() {
    inputRef.current.focus()
  }

  return (
    <div>
      App
      <h2 ref={titleRef}>App</h2>
      <input type="text" ref={inputRef} />
      <button onClick={getTitleDom}>获取h2的Dom</button>
      <button onClick={getFocus}>输入框获取焦点</button>
    </div>
  )
})

export default App

useMemo

useMemo实际的目的也是为了进行性能的优化。

  • useMemo返回的也是一个 memoized(记忆的)值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

优化的场景:

  • 进行大量的计算操作,是否有必须要每次渲染时都重新计算;
  • 对子组件传递相同内容的对象时,用useMemo进行性能优化;

a. useMemo与useCallback的区别:

  • useMemo返回的是一个值;
  • useCallback返回的是一个函数;

下面两条语句作用相等

const foo = useCallback(fn, [])

const foo2 = useMemo(() => fn, [])

import React, { memo, useState, useMemo } from 'react'

function calcCountTotal(num) {
  console.log('计算被调用')
  let res = 0
  for (let i = 0; i <= num; i++) {
    res += i      
  }
  return res
}

const HelloWorld = memo((props) => {
  console.log('子组件被渲染')
  return (
    <div>
      <h2>Hello World组件</h2>
    </div>
  )
})

const App = memo(() => {
  console.log('组件被渲染')
  const [count, setCount] = useState(10)
  const [num, setNum] = useState(0)

  // 只有count发生改变时,才会回调这个useMemo中的回调函数
  const result = useMemo(() => {
     return calcCountTotal(count)
  }, [count])
  // const result = calcCountTotal(10)
  
  function addCount() {
    setCount(count + 1)
  }

  function addNum() {
    setNum(num + 1)
  }

  const info = useMemo(() => ({name: 'lyx', age: 18}), [])

  return (
    <div>
      Memo
      <div>{count}的阶乘:{result}</div>
      <button onClick={addCount}>+1</button>
      <hr />
      <div>{num}</div>
      <button onClick={addNum}>+1</button>
      <hr />

      {/* 使用useMemo优化后,只要info对象不发生改变,子组件就不会重新渲染 */}
      <HelloWorld info={info} />
    </div>
  )
})

export default App

可以看到只有count发生改变时才会重新调用 calcCountTotal 函数,如果不适用 useMemo 对值进行优化,那么在 num 发生改变时,也会重新调用calcCountTotal 函数,这是会影响到性能的。

并且使用 useMemo 优化 info 后,只要 info 对象不发生改变,子组件就不会重新渲染。