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>
  );
});
相关推荐
web1478621072318 分钟前
C# .Net Web 路由相关配置
前端·c#·.net
m0_7482478019 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖22 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案130 分钟前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_7482548835 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.1 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营1 小时前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui