likes
comments
collection
share

深入了解 setState 和 useState

作者站长头像
站长
· 阅读数 4

setState

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

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

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

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

用法

this.setState(nextState, [callback])

setState 源码

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?

  • 方式一:❌
    handleClick = () => {
      for (let i = 0; i < 20; i++) {
        this.setState({
          count: this.state.count + 1,
        });
      }
    }
    
    在每一次循环的时候,count 值并没有被更新,只是把修改的任务放在了队列中,所以每一轮循环,我们拿到的 this.state.count 都是 0。 所以导致放入队列中的任务都是要把 count 修改为 1。

深入了解 setState 和 useState

  • 方式二:✅
    handleClick = () => {
      for(let i = 0; i < 20; i++) {
        this.setState(prevState => {
          return {
            count: prevState.count + 1
          }
        });
      }
    }
    
    这种方式是把回调函数放入队列中,最终依次执行函数,得到结果 count = 20

深入了解 setState 和 useState

setState 是同步还是异步的

React18 之前

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

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

如果 setState 是同步的,就会造成执行几次 setState,就渲染几次页面,造成严重的性能消耗。

React18

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

目的:实现状态的批处理

好处

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

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

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

import {flushSync} from 'react-dom';

flushSync(callback);

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

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

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

useState

useState 是一个 React Hook,它允许向组件添加一个或多个状态变量。 解决了函数组件没有状态的问题。

用法

组件的顶层或自己的 Hook 中调用 useState 来声明一个或多个状态变量。 使用数组解构的方式来命名状态变量。

import { useState } from 'react';

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

参数

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

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

返回

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

深入了解 setState 和 useState 注意:

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

set 函数

接收任何类型的值。

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

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

useState 原理

useState 源码(简化版):

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 中,每个组件都有一个对应的 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 官方文档

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

深入了解 setState 和 useState