likes
comments
collection
share

为什么我在setState后读取不到最新的state

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

针对于这个问题, 我们可以先看一段代码

function handleClick() {
  console.log(count);  // 0

  setCount(count + 1); // 请求使用 1 重新渲染
  console.log(count);  // 仍然是 0!

  setTimeout(() => {
    console.log(count); // 还是 0!
  }, 5000);
}

这边React给的解答是: set函数不能改变运行中代码的状态 , 更新状态会使用最新的值请求另一个渲染, 但并不影响已经运行的事件处理函数中的变量

OK针对于上面这句话我们来做一个简单的解答: 就是说React会用新的state值去请求新的一个渲染, 但你正在运行的函数中的值不会受到影响

React会用新的state值去请求新的一个渲染

针对于上半句话我们来看一段代码

import { useState } from 'react';

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  if (isSent) {
    return <h1>Your message is on its way!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <button type="submit">Send</button>
    </form>
  );
}

function sendMessage(message) {
  // ...
}

如果对这段代码进行运行, 那么我们可以理解react解答的前半句, 当我们更新state时, 他会用最近的值去请求一个渲染, 也就是他的dom展示会变成<h1>Your message is on its way!</h1>

这边我们再引出另外一个知识点: state表现出来的特性更像是一张快照, 当组件被调用时, 组件会为那一次渲染提供一个state快照, 然后组件会根据这个state快照计算出来的值来执行渲染

这边我们再看另一段代码, 可以解释这个快照的概念

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
     
      }}>+3</button>
    </>
  )
}

这个组件的运行结果最终是: 每点击一次, number的数量并不是+3而+1, 这个其实就解释了, 在该次渲染前, state提供给组件的渲染快照中的值是0 , 每一次set, 都是在上一次快照的基础上+1, 那么每一个set函数最后呢执行出来的结果都是1, 所以下一次给到渲染的state值也就是0+1, 循环往复每一次点击都是在上一次快照的基础上+1

接下来我们再去解释后半句

正在运行的函数中的值不会受到影响

已知紧跟在set函数后面输出state值不会是更新后的值, 那么我们给他加一个定时器让他晚一点再输出, 说不定晚点输出后他就更新完了呢?


import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )
}

这段代码的运行结果:UI中渲染的值马上变成了 5 , 定时器结束后页面弹出 0 那么我们继续增加定时器的时间后, 其实可以发现, 异步并不能解决我们的问题, 这个0似乎是运行这个方法时就已经决定了他是0, 也就是说, 一个state变量的值永远不会在一次渲染的内部发生变化, 这其实也就解释了后半句, 正在运行中的函数不会受影响, 既然这一次运行中的state都不会发生变化, 那么也自然就不会更新了

了解了原因和运行机制后, 我们再来看看怎么去解决这个问题:

如果我们需要使用下一个状态, 可以将他传递给set函数之前保存在一个变量中, 再将保存的变量传递给set函数, 也就是用来一个变量来维持同样的值使用

const nextCount = count + 1;
setCount(nextCount);

console.log(count);     // 0
console.log(nextCount); // 1

OK , 到这里我们其实已经知道了标题问题的原理和解决方案, 那么我们再看一个引申的问题

在上面的代码中, 有一段代码是对state做了三次setNumber(number + 1);的操作, 最后的结果并没有变成+3而是+1

那么我们怎么处理在一次事件更新同一个state进行多次更新呢, 比如+1+1+1这样, 最简单的方法当然是一步到位直接+3, 这是一个不常见的用例, 但是这一块其实可以引出一个批处理的概念

React会等到事件处理函数中的所有代码都运行完毕后再处理state更新

光看这段可能不是很好理解, 我们参照上面的代码来看

 <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
}} />

在上述代码中, react会形成一个队列, 以便在事件处理函数中的其他事件运行完毕后再去处理state操作, 也就是这边队列中会存放三个 setState(0+1)

那么我们如果想让这个队列变成

setState(0+1)
setState(1+1)
setState(2+1)

也就是我们最终预期的+3结果, 我们需要借助一个更新函数

setNumber(n => n + 1);  
setNumber(n => n + 1);  
setNumber(n => n + 1);

这个时候react放进更新队列中的就不是0+1而是n => n + 1, 他将0作为入参传入第一个更新函数, 得到0+1也就是1, 然后将上一个更新函数的返回值作为入参传递给下一个更新函数也就是1+1, 以此类推我们可以最终实现我们的+3的需要. 但是这边建议直接用setState(number+3). 当然这只是一个简单case下的实现, 如果有更复杂硬要拆分的需要时再使用更新函数来完成这个操作