useState原理

192 阅读4分钟

useState是React中的一个Hook,用于函数组件中添加状态管理。

1、useState的基本用法

useState返回一个状态变量root和一个更新状态的函数setCount

import React, { useState } from "react";

function Counter() {
  // 声明一个新的状态变量 "count",初始值为 0
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

2、useState的实现原理

2.1 Hook的内部机制

在React内部,每一个组件都有一个fiber对象,其中包含与该组件相关的所有信息(状态、效果等)。当你使用useState时,React会在这个fiber对象中创建一个新的状态单元(state unit)。

2.2 状态存储与更新

React内部维护一个链表,用于存储每个组件的Hook状态。当组年首次渲染时,React会遍历这个链表并初始化状态。当组件重新渲染时,React会遍历链表并更新状态。

let currentHook = null;

function useState(initialValue) {
  if (!currentHook) {
    // 第一次渲染时初始化 state
    currentHook = {
      state: initialValue,
      next: null,
      setState: function (newState) {
        currentHook.state = newState;
        // 触发重新渲染
        render();
      }
    };
  }
  // 返回当前状态和更新状态的方法
  return [currentHook.state, currentHook.setState];
}

function render() {
  // 在实际 React 中,这里会重新渲染组件并更新 DOM
}

2.3 Hook的调用顺序

一个关键点是Hook的调用顺序必须保持一致。React使用这个顺序来关联状态和组件。如果调用顺序发生变化,状态就会错位,导致Bug。因此,Hook必须在函数组件的顶层调用,不能在循环、条件语句或嵌套函数中调用。

3、useStatesetState为什么是异步的

setState的异步性在React中是一个常见的特性,主要是为了优化性能和确保一致性。

3.1 性能优化--批量处理

React通过批处理(batching)多次setState调用来减少重新渲染次数。如果每次状态更新都同步执行,可能会导致不必要的多次渲染,影响性能。

const [count, setCount] = useState(0);
function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  console.log(count)  // 输出0,界面显示为1
  // 如果是同步的,组件会渲染三次
  // 但由于批处理,组件只会渲染一次
}

在上述代码中,虽然调用了三次setState,但是由于React批处理更新,组件只会在批处理结束后重新渲染一次。

3.2 保证状态一致性 --状态的时序性

setState是异步的,以确保状态更新后的视图是最新的。如果setState是同步的,每次更新都会立即触发渲染,这可能会导致状态不一致,尤其是在异步操作或事件处理函数中。

3.3 React的内部机制

3.3.1 虚拟DOM的应用

React通过虚拟DOM来进行高效的更新和渲染。在一次事件循环中,React会收集所有的状态更新,并在下一次事件循环中批量处理这些更新,从而计算出最小的DOM变化。

3.3.2更新队列

React在内部维护一个更新队列,当调用setState时,更新会被推入队列,而不会立即执行。这允许React在同一个事件循环中对多个状态更新进行批处理,从而提高性能和更新的效率。

3.4 实际操作中的异步

虽然setState是异步的,但在某些情况下,它可能表现为同步的。例如:在useEffectcomponentDidMount这些生命周期方法中,setState的行为可能看起来是同步的,这是因为这些方法已经位于更新队列的末尾,下一次更新会立即执行。

4、 setState在什么情况下是同步更新?

  1. 在useEffect中是同步更新
import React, { useState, useEffect } from 'react';

const ExampleComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(count); // 初始值
    setCount(count + 1);
    console.log(count); // 更新后的值,在 useEffect 中,更新是同步的
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
};

  1. 在React18中,引入自动批处理功能
import React, { useState, useEffect } from 'react';

const ExampleComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(count); // 初始值
    setCount(count + 1);
    console.log(count); // 更新后的值,在 useEffect 中,更新是同步的
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
};

5、setState发生了什么?

  1. 调用enqueueSetState更新入口
  2. 调用requestUpdateLane取得本次更新的优先级
  3. 此处返回的lanes会作用全局渲染的优先级,用于fiber树构造过程中,针对fiber对象或update对象,只要它们的优先级(如:fiber.lanes和update.lane)比渲染优先级低,都会被忽略。
  4. 进入更新
  • 更新分为2种情况,是同步更新模式还是并发更新模式。
  • 如果是同步更新模式,将在微任务下进行异步更新。
  • 如果使用到了useTransition、useDeferredValue等进入低优先级并发更新模式。这种模式在宏任务中进行异步渲染。宏任务基于postMessage实现的。为什么不用setTimeout?因为时间不准确。