learn by coderWhy(REACT)

关于setState性能优化

js 复制代码
当你调用setState时,即使更改的数据与原来的数据是一样的,它仍然会去调用render函数!

// 这种优化我们通常称之为SCU
所以有个shouldComponentUpdate()函数给我们做优化,若数据相同,return false,即可不触发render()生命周期

在类组件中,React也想到了这一层,所以如果我们不想繁琐的写SCU去优化的话,那么我们可以将类不是继承React的Component,而是React的PureComponent
import { PureComponent } from 'react'
export class demo extends PureComponent {}
export default demo


函数组件中也有类似PureComponent的,
import { memo } from "react"
const Test = memo(function(props) {
    console.log('Test render')
    return <div>Test</div>
})
export default Profile

所以这里也就牵扯到为什么修改state数据时,要保持它的不可变性,更改数据,保持replace的原则,而不是modify;
例子:
// 这样写,如果继承的是React的Component,你也能成功更新渲染,但是如果继承的是React的PureComponent,那么就不会更新成功,因为新旧state是一模一样的,那就不会触发render函数
this.state = { arr:[1,2,3] }
this.state.arr.push(4)
this.setState({ arr: this.state.arr })
js 复制代码
// pureComponent底层是类似使用这样的方法进行优化的:
shouldComponentUpdate(nextProps, nextState) {
    return (
        !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state)
    )
}

其进行的是浅层比较,如果props或state有变化的话,才return true

setState更新数据的原理

js 复制代码
this.state = {msg: 'msg', num: 0}
this.setState({msg: 'newMsg'})

但是num数据还会在,其原理层面上,
react在更新数据时使用的是Object.assign(this.state, newState), // 新数据会覆盖旧数据中相同属性的值
并非是this.state = newState 这种直接覆盖值的操作。
在Object.assign()处理完数据后,再调用render()函数更新渲染

为什么setSate是异步的?(但是需要分情况,不一定是异步,详见下面主题:setState一定是异步吗?)

在GitHub上,react的核心成员(redux的作者gaearon)有作过这样的一个回答(issue

js 复制代码
简单的总结就是:

setState设计为异步,可以`显著的提升性能`;
1.如果每次调用setState都进行一次更新,那么就意味着render函数会被频繁调用,界面重新渲染,这样的的效率非常低;
2.最好的办法应该是获取到多个更新,之后进行批量更新;

`如果同步更新了state,但是还没有执行render函数,那么state和props就不能保持同步了`
1.state和props不能保持一致性,就会在开发中产生很多的问题;
2.所以最好是setState后,尽快执行render函数;(虽然有一种setState后,立马手动this.render()去执行,
但是这样就会造成很多性能上的问题,且setState后,可能还有一些逻辑代码需要写)

例子:在App组件中setState了,如果是同步更新的话,那么setState后,
this.state就立马更新了,但如果在后面的代码中出错了,没有执行render函数,
那么App的子组件Son,Son组件中props仍然是之前的数据(因为没有调用render函数),
导致数据不同步,后续就会发生很多的问题。

setState一定是异步吗?

这就需要分两种情况了

React18之前

js 复制代码
// 在组件生命周期或React合成事件中,setState是异步;
// 在setTimeout或原生dom事件或promise.then中,setState是同步;

changeName() {
    setTimeout(() => {
        this.setState({name: 'newName'})
        console.log(this.state.name); // newName
    }, 0);
}

componentDidMount() {
    const btn = document.getElementById('btn');
    btn.addEventListener('click', () => {
        this.setState({
            name: 'newName'
        })
        console.log(this.state.name); // newName
    })
}

React18之后

setState默认都是异步(批处理)的了,即使在setTimeout或原生dom事件或promise.then中。

于官网中的介绍

查看更多关于React18中,更少使用render函数的自动批处理

js 复制代码
// 但是使其同步仍然是有办法的,引入react-dom中{ flushSync }

import { flushSync } from 'react-dom'

changeName() {
    flushSync(() => {
        // 这里的3个setState仍然会是批处理
        this.setState({name: 'newName'})
        this.setState({name: 'newName'})
        this.setState({name: 'newName'})
        console.log(this.state.name) //oldName
    })
    console.log(this.state.name) // newName
}

diff算法的优化

首先如果一棵树参考另外一棵树进行完全比较更新,第一个虚拟 DOM 树有 m 个节点,第二个虚拟 DOM 树有 n 个节点,那么该算法的复杂程度为O(m * n),如果使用该算法,那么展示的元素过多时,需要执行的计算量将非常大,该更新性能会非常低效;

于是React对算法进行了优化,将其优化成O(n)

js 复制代码
在一般情况下,React 的 diff 算法的时间复杂度可以近似地看作是 O(n),其中 n 是虚拟 DOM 树中的节点数量。

但是在非常深的树结构或存在大量兄弟节点的情况下,算法的复杂度可能会增加到 O(n^3)。为了应对这种情况,
React 引入了最大深度限制和时间片(Time Slicing)等机制,以确保算法的执行时间不会超过某个阈值,
从而保持用户界面的响应性。

具体的复杂度可能受到树结构和优化策略的影响,在一般情况下能够提供高效的更新操作。

那是如何优化的呢

  1. 同层节点之间相互比较,不会跨节点比较;
  2. 不同类型的节点,产生不同的树结构;(比如:一个div节点,变成了p节点,那么其所有子节点这个树,都会重新生成)
  1. 还有开发中,我们也会用key来指定哪些节点在不同的渲染下保持稳定;(比如:同层级中(a、b、c),在中间多了一个新节点(a、x、b、c),它并不会把a后面的所有节点都替换掉,根据key,如果能复用,就继续复用(仅进行位移就好),只会在索引为1中插入新节点)

useSelector的性能优化(shallowEqual)

useSelector是根据整个state去判断当前组件是否需要重新渲染,比如:修改父组件中redux的某个数据,(子组件没有用到,但仍触发子组件的重新渲染)

为了避免没必要的重新render,我们只需要在每次用到useSelector,就给该hook中的第二个参数写上 shallowEqual 即可

js 复制代码
import { shallowEqual, useDispatch, useSelector } from "react-redux";

// 子组件
const Home = memo((props) => {
  // 给useSelector加上浅层比较(shallowEqual),就不会频繁触发重新渲染
  const { msg } = useSelector(
    (state) => ({
      msg: state.counter.msg,
    }),
    shallowEqual
  );

  const dispatch = useDispatch();
  function handleChangeMsg() {
    dispatch(changeMsgAction(Math.random().toString()));
  }

  console.log("Home render");

  return (
    <div>
      <h2>Home</h2>
      <div>msg:{msg}</div>
      <button onClick={(e) => handleChangeMsg()}>改变msg</button>
    </div>
  );
});

// 父组件
const App = memo((props) => {
  const { counter } = useSelector(
    (state) => ({
      counter: state.counter.counter,
    }),
    shallowEqual
  );

  const dispatch = useDispatch();

  function handleSetCounter(num, isAdd = true) {
    if (isAdd) {
      dispatch(addCounterAction(num));
    } else {
      dispatch(subCounterAction(num));
    }
  }

  console.log("App render");

  return (
    <div>
      App
      <div>counter: {counter}</div>
      <div>
        <button onClick={(e) => handleSetCounter(2)}>+2</button>
        <button onClick={(e) => handleSetCounter(3, false)}>-3</button>
      </div>
      <Home />
    </div>
  );
});
相关推荐
2301_80107415几秒前
TypeScript异常处理
前端·javascript·typescript
小阿飞_2 分钟前
报错合计-1
前端
caperxi3 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男3 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189114 分钟前
前端css样式覆盖
前端·css
学习路上的小刘6 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&6 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白17 分钟前
react 组件通讯
前端·react.js
Redstone Monstrosity34 分钟前
字节二面
前端·面试
东方翱翔41 分钟前
CSS的三种基本选择器
前端·css