深入了解 setState 和 useState

setState

类组件 中,通过调用 setState 函数来更新 React 组件的 state。

在类组件中调用 setState 等同于在函数组件中调用 set函数

在 React 类组件中,为什么修改状态要使用 setState 而不是用 this.state.xxx = xxx?

  • 直接修改 this.state.xxx = xxx 不会触发组件渲染,React 无法监听到状态的变化,从而无法更新视图。
  • setState 提供了异步更新、合并状态、批处理、触发生命周期和回调函数等功能。

用法

this.setState(nextState, [callback])

  • nextState:一个对象或者函数。

    jsx 复制代码
    // 1. nextState 是对象:
    // 支持部分状态修改,它将浅层合并到 this.state
    this.setState({
      count: this.state.count + 1  // 不论总共有多少种状态,我们只修改了 name,其他状态不会被改变
    });
    
    // 2. nextState 是函数:
    // 它被视为更新函数,必须是一个纯函数,接收已加载的 state 和 props 作为参数,将返回结果浅层合并到 state 对象中。
    this.setState((prevState, prevProps) => {
      return {
        count: prevState.count + 1,
      };
    });

    注意:

    • 如果提供的新值和旧值相同(通过 Object.is 比较),React 将跳过本次重新渲染该组件及其子组件,可以理解为这是 React 性能优化的一种机制。
  • callback :可选,回调函数。在状态更新完毕(componentDidUpdate)后,触发执行。
    只要执行了 setStatecallback 一定会执行。

    类似于 Vue 中的 $nextTick

    jsx 复制代码
    state = {
      count: 0
    }
    this.setState({
      count: this.state.count + 1
    }, () => {
      // 可以获取最新的 state 对象({ count: 1 })
    });

    注意:

    • 即便我们基于 shouldComponentUpdate 阻止了视图函数的更新,componentDidUpdate 周期函数肯定不会执行了,但是 callback 回调函数依然会被触发执行。

setState 源码

js 复制代码
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

场景题:for 循环 20次 setState,如何只渲染一次,让 count 变成 20?

  • 方式一:❌

    jsx 复制代码
    handleClick = () => {
      for (let i = 0; i < 20; i++) {
        this.setState({
          count: this.state.count + 1,
        });
      }
    }

    在每一次循环的时候,count 值并没有被更新,只是把修改的任务放在了队列中,所以每一轮循环,我们拿到的 this.state.count 都是 0
    所以导致放入队列中的任务都是要把 count 修改为 1。

  • 方式二:✅

    jsx 复制代码
    handleClick = () => {
      for(let i = 0; i < 20; i++) {
        this.setState(prevState => {
          return {
            count: prevState.count + 1
          }
        });
      }
    }

    这种方式是把回调函数放入队列中,最终依次执行函数,得到结果 count = 20

setState 是同步还是异步的

React18 之前

在 React18 之前,setState 在不同情况下可以表现为同步或异步。

  • 在 Promise 的状态更新、js 原生事件、setTimeout、setInterval 中是同步的。
  • 在 React 的合成事件、生命周期函数中是异步的。

React18

在 React18 中,setState 在任何地方执行都是异步的。

目的 :实现状态的批处理

好处

  • 减少视图更新的次数,降低渲染消耗的性能。
  • 让更新的逻辑和流程更清晰,更稳健。

原理 :利用了更新队列(updater)机制来处理。

  • 在当前相同的时间段内(浏览器此时可以处理的时间中),遇到 setState 会立即放入到更新队列中;
  • 此时状态/视图还未更新;
  • 当所有的代码操作结束,会刷新队列(通知更新队列中的任务执行):把所有放入的 setState 合并在一起执行,只触发一次视图更新(批处理操作)。

练习:以下代码输出什么,渲染了几次页面?

jsx 复制代码
state = {
  x: 1,
  y: 2,
  z: 3
}

handleClick = () => {
  this.setState({ x: this.state.x + 1 });
  this.setState({ y: this.state.y + 1 });
  console.log('第一个 state:', this.state);
  
  setTimeout(() => {
    this.setState({ z: this.state.z + 1 });
    console.log('第二个 state:', this.state);
  }, 1000);
}



// 第一个 state:{x: 1, y: 2, z: 3}
// 第二个 state:{x: 2, y: 3, z: 4}
// 渲染了两次页面,因为 前两个 setState 合并渲染一次,setTimeout 的回调函数被放到了下一次事件循环中,1000ms 时间到了之后,再渲染一次

flushSync

flushSync 允许强制 React 同步刷新回调函数中的任何更新。

jsx 复制代码
import {flushSync} from 'react-dom';

flushSync(callback);

例如上面的场景题如果改成,点击一次按钮,实现页面刷新 20 次,并且 count 变成 20,就可以使用 flushSync 实现:

jsx 复制代码
handleClick = () => {
  for (let i = 0; i < 20; i++) {
    flushSync(() => {
      this.setState({
        count: this.state.count + 1,
      });
    });
  }
}

// 执行一次 handleClick,页面会渲染20次,count 变成 20

注意:

  • flushSync 会严重影响性能,请谨慎使用。
  • flushSync 可能会强制挂起 Suspense 边界显示其 fallback 状态。

useState

useState 是一个 React Hook,它允许向组件添加一个或多个状态变量。

解决了函数组件没有状态的问题。

用法

组件的顶层或自己的 Hook 中 调用 useState 来声明一个或多个状态变量。

使用数组解构的方式来命名状态变量。

jsx 复制代码
import { useState } from 'react';

const App = () => {
  const [something, setSomething] = useState(initialState);
};

参数

  • initialState:state 初始化的值,可以是任意类型。在初始渲染之后,此参数将被忽略。
jsx 复制代码
// 1. 非函数类型的值
const [num, setNum] = useState(0);
const [arr, setArr] = useState([]);

// 2. 函数类型的值
// 它被视为初始化函数,应该是纯函数,将返回值作为状态的初始值。当初始化组件时,React 将调用该初始化函数,并将返回值存储为初始状态。
const [count, setCount] = useState(fn);

返回

  • 当前的 state:首次渲染时,与传递的 initialState 匹配。
  • set 函数:可以将 state 更新为不同的值并触发重新渲染。

注意:

  • 不能在循环或条件语句中调用它。如果你需要这样做,请提取一个新组件并将状态移入其中。
  • 在严格模式中,React 将两次调用初始化状态,以找到意外的不纯性。这只是开发环境的行为,不会影响生产。

set 函数

接收任何类型的值。

jsx 复制代码
// 1. 非函数类型的值
// 因为状态被认为是只读的,如果状态是数组或对象,应该去替换它,而不是改变它。
setCount(count + 1);
setArr([...arr, ...newArr]);

// 2. 函数类型的值
// 更新函数,纯函数,只接收待定的 state 作为其唯一参数,并返回下一个状态。React 把更新函数放入队列中,进行批处理,并重新渲染组件。
setCount(count => count + 1);
  • set 函数仅更新下一次渲染的状态变量,状态表现为就像一个快照。如果在调用 set 函数后读取状态变量,仍会得到更新前的值。
  • 如果提供的新值和旧值相同(通过 Object.is 比较),React 将跳过本次重新渲染该组件及其子组件,可以理解为这是 React 性能优化的一种机制。
  • React 会批量处理状态更新 ,可参考 setState 的批处理及异步处理方式。

useState 原理

useState 源码(简化版):

js 复制代码
let state = [],
    index = 0;
const defer = (fn) => Promise.resolve().then(fn);
function useState(initialValue) {
  // 保存当前的索引;
  let currentIndex = index;
  if (typeof initialValue === "function") {
    initialValue = initialValue();
  }
  // render时候更新state
  state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
  const setState = newValue => {
    if (typeof newValue === "function") {
      // 函数式更新
      newValue = newValue(state[currentIndex]);
    }
    state[currentIndex] = newValue;
    if (index !== 0) {
      defer(renderComponent);
    }
    index = 0;
  };
  index += 1;
  return [state[currentIndex], setState];
}

怎么保证一个组件中写多个 useState 不会串?

React Hook 是通过 Fiber 架构和调度器来实现的,保证了在一个组件中写多个 useState 不会串。

在 React 中,每个组件都有一个对应的 Fiber 树,React 使用 Fiber 架构来实现组件的调度和更新。每个 Fiber 节点都包含了组件的状态、属性、子节点等信息。当组件重新渲染时,React 会创建一个新的 Fiber 树,然后通过协调器算法比较新旧 Fiber 树的差异,并根据差异来更新 DOM。

在 Hook 中,每个 useState 调用都会生成一个对应的 Hook 对象,并将其存储在组件对应的 Fiber 节点(memorizedState 属性)中。这样,每个 useState 调用都有自己的状态和更新函数,它们是相互独立的,不会共享状态。

当组件重新渲染时,React 会根据 Hook 在 Fiber 节点中的顺序依次执行,以保证 useState 调用的顺序和对应的状态值和更新函数一一对应。这样就确保了在一个组件中写多个 useState 不会串,每个 useState 调用都能正确地管理自己的状态。

总之,React 使用 Fiber 架构和调度器来保证 Hook 在函数组件中的正确执行顺序,并根据 Hook 的调用顺序来管理状态,从而确保在一个组件中写多个 useState 不会串。

函数组件重新渲染的时候怎么拿到 useState 之前的状态,而不是得到初始化的状态?

根据上面的 useState 简化版源码,我们可以看到每次在函数组件中调用 useState 时,都会判断当前状态是否被初始化,如果已经被初始化,React 就会返回上一次 useState 调用存储的状态值。

总结

setStateuseState 很多地方是相似的。

相同点:

  • 都是用到了队列机制;
  • 都是异步更新,批处理状态更新;
  • 都是通过 Object.is 进行新旧值的判断,来确定是否要重新渲染该组件及其子组件。

区别:

  • setState 是类组件中更改状态,useState 是函数组件中更改状态;
  • 语法不同。

参考:setState 官方文档
参考:useState 官方文档

欢迎纠错及表达自己的观点~

相关推荐
黄尚圈圈30 分钟前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水1 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
小飞猪Jay5 小时前
C++面试速通宝典——13
jvm·c++·面试
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光5 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   5 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   5 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web5 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常5 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式