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>
  );
});
相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试