React useState异步代码没有更新?

背景:

React代码如下

import { useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(count, 'end'); // 还是 0!
    }, 5000);
  }
  return (
    <>
      <button onClick={handleClick}>+1</button>
      <span>{count}</span>
    </>
  );
}

setCount的更新是异步更新的,setTimeout也是异步的,为什么点击,console.log打印的是0,而不是1。

尝试:

尝试用普通js函数进行比较,js代码如下

function outerFunction() {    
  let count = 0;  
    
  function innerFunction() {    
    console.log(count);  
  }    
    
  count = 1;  // 将count更新为1  
  setTimeout(innerFunction, 5000); 
    
  count = 2; 
}    
  
outerFunction();

console.log打印的是最新的值2。

疑问: 两段代码的打印结果不同,是什么原因呢?

阅读 3.3k
avatarAI BotBETA

在React中,useState的更新是异步的,这意味着当你调用setCount函数时,count的值不会立即改变。相反,React会等待当前的渲染周期完成,然后在下一个渲染周期中更新count的值。

在你的React代码示例中,当你点击按钮时,setCount函数会被调用,count的值会被计划在下一个渲染周期中更新。然后,setTimeout函数会在5秒后执行,此时count的值还没有被更新,所以console.log打印的仍然是旧的值0。

而在你的JavaScript代码示例中,当你调用count = 1count = 2时,count的值会立即改变,所以在5秒后执行innerFunction时,console.log打印的是最新的值2。

为了解决这个问题,你可以在setTimeout的回调函数中直接使用setCount函数来获取最新的count值。这样,无论count何时更新,setTimeout的回调函数中都能获取到最新的值。以下是修改后的React代码示例:

import { useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(count, 'end'); // 使用setCount获取最新的值
    }, 5000);
  }
  return (
    <>
      <button onClick={handleClick}>+1</button>
      <span>{count}</span>
    </>
  );
}

在这个修改后的示例中,setTimeout的回调函数中使用了count而不是setCount来获取最新的值。当count的值更新时,setTimeout的回调函数中也会获取到最新的值,因此console.log会打印出正确的值。

5 个回答
setTimeout(() => {
  console.log(count, 'end'); // 还是 0!
}, 5000);

你可以把react组件的每次渲染理解为生成了一个平行宇宙。

初始化渲染时生成的平宇宙,其count值为0,所以就算定时器延迟一年执行,它也只能输出0

当你调用setCount(count + 1),这触发了第二次渲染,又生成了一个平行宇宙,这个平行宇宙中的count1,这个宇宙中的定时器也就只能输出1

去掉定时器帮助理解

const handleClick = () => {
  setCount(count + 1);

  console.log(count, 'end');
}
// 两者其实是一样的,加上定时器并不会让count拥有穿透平行宇宙的能力
const handleClick = () => {
  setCount(count + 1);
  setTimeout(() => {
    console.log(count, 'end');
  }, 5000);
}

当你setState时,组件会rerender,所谓rerender其实就是App这个function重新执行了一次,重新执行时你通过useState这个hook取到是最新值,但是你上一次App的函数调用中的setTimeout的回调中的count因为闭包的关系访问的仍然是当时的值,转换成下面这种更接近:

var value;

function useState(val) {
    return [value ??= val, v => {
        value = v;
        App();
    }]
}

function App() {    
    const [count, setCount] = useState(0);
    const handleClick = () => {
      setCount(count + 1);
      setTimeout(() => {
        console.log(count, 'end'); // 还是 0!
      }, 5000);
    }
    console.log('render', count)
    return {
        onClick: handleClick,
    }
}    

App().onClick()

首先,setCount 的确是异步更新的。当你调用 setCount(count + 1) 时,React 会将状态更新的请求排队,但不会立即执行状态更新。这意味着在当前的执行上下文中,count 的值不会立即改变,直到 React 的下一个渲染周期。

然后,setTimeout 中的回调函数也是异步执行的,它会在指定的延时后加入到 JavaScript 任务队列中等待执行。但关键点在于,setTimeout 的回调函数中访问的 count 值是被其外层函数作用域捕获的。在 handleClick 函数执行时,count 的值是在函数被调用时的快照,这个值在 setTimeout 的回调函数中不会因为外部的 setCount 调用而改变。

所以,当你点击按钮触发 handleClick 时,setCount(count + 1) 将一个状态更新排入队列(但在此刻不会立即执行更新),而 setTimeout 的回调函数则捕获了当前的 count 值(即 0),并且在 5 秒后打印出来。即使 React 在此期间更新了 count 的状态,setTimeout 的回调仍然会打印出它被定义时捕获的 count 值。

要想在 setTimeout 中访问最新的 count 值,你可以这样。

`
import { useState } from 'react';

export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {

setCount(prevCount => {
  const updatedCount = prevCount + 1;
  // 更新状态后,设置了一个延时函数
  setTimeout(() => {
    console.log(updatedCount, 'end'); // 这里将打印出更新后的 count 值
  }, 5000);
  return updatedCount;
});

}
return (

<>
  <button onClick={handleClick}>+1</button>
  <span>{count}</span>
</>

);
}
`

官方文档中提到了这个问题,其实就是在重新渲染之前,此刻的state相当于一个快照,传入了你所有调用的代码,也就是这里settimeout挂起异步处理时,就决定了传入的count为0

首先这里是const,不会改变的。是let的话也不会改变,除非是一个object,才可能会改变。

现在它可以输出1了。

import { useState } from 'react';

function Count() {
  const [count, setCount] = useState({ count: 0 });

  const handleClick = () => {
    setCount((pre) => {
      pre.count += 1;
      return pre;
    });
    setTimeout(() => {
      console.log(count.count, 'end'); // 还是 0!
    }, 5000);
  };
  return (
    <>
      <button onClick={handleClick}>+1</button>
      <span>{count.count}</span>
    </>
  );
}

export default Count;
推荐问题
宣传栏