UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • React三大基础Hook使用
  • 关于严格模式下useEffect执行两次的问题
  • 从零搭建一个React管理系统
  • useEffect闭包陷阱
  • react路由切换过度效果
  • React如何获取最新状态
    • 前言
    • 问题分析
    • 解决方案
    • 结语
  • 《React18》
hanhanbuku
2025-12-03
目录

React如何获取最新状态

# 前言

react在引入了函数式组件和hooks后,如果js基础不够夯实,总是会遇到一些奇奇怪怪的问题,比如本文要说的,setState之后无法获取到最新的state值。本文将通过js原理一步一步剖析问题产生的原因以及解决办法

# 问题分析

提示

接下来先看看问题大致是什么样的

先来看一段代码

function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 这里打印的是更新前的值,而不是更新后的值
    cb()
  };
  
  const cb = () =>{
      console.log(count) // 这里打印的是依旧是更新前的值,而不是更新后的值
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

示例中,我们在handleClick函数里调用了setCount,并且设置了旧值+1

理论上来说页面上渲染的count应该变为1,并且handleClick里打印的count也是1,但实际情况是渲染的count确实变成1了,但是打印的count依旧是0

那到底是什么原因造成的这一问题呢?

其实从代码上来看,还是一眼就能看出端倪的。首先整个MyComponent在运行的过程中产生了闭包函数,说到闭包函数,我们先简单回忆一下闭包函数产生的因素:

  1. 在函数里创建函数(下文称这个函数位子函数)
  2. 在子函数中访问父函数中的变量。

例子中handleClick,和cb都是一个闭包函数,他们访问了MyComponent函数中的count变量。到这里,可能大家会觉得疑惑了,没问题啊是闭包了,是引用count了但是为什么拿不到最新的count值呢?正常闭包引用变量的话也是可以拿到最新值的吧。

下面我们再看一个最基础的闭包例子

function name() {
    let a = 1           // 第1步:声明a
    const fn = function() {  // 第2步:创建fn(闭包)
        console.log(a)  // 捕获了变量a的引用
    }
    a = 2               // 第3步:修改a
    return fn           // 第4步:返回fn
}

const myFn = name()
myFn() // 输出2,不是1!
1
2
3
4
5
6
7
8
9
10
11

你看,这个例子中不就拿到最新的值了吗?

接下来需要大家的思维开始抽象一点了,首先正常的情况下闭包捕获的变量确实是可以拿到最新值得,因为捕获得是变量得引用。但是react的函数式组件有一点特别,他更新视图并不是靠的什么引用变量,响应式更新视图。 而是最暴力的直接重新调用整个函数,也就是重新执行函数里的所有代码。

再回到上面的普通闭包的例子,可以看到整个过程中name只调用了一次,a始终都是那个a。

但是react的函数组件是怎么重新渲染的呢:MyComponent()->setCount()->MyComponent().

可以看到在调用了setCount之后,又重新执行了MyComponent。把这个型为带入到普通的闭包例子里就是

function name() {
    let a = 0           // 第1步:声明a
    const fn = function() {  // 第2步:创建fn(闭包)
        a++               // 第3步:修改a
        console.log(a)  // 捕获了变量a的引用
    }
    return fn           // 第4步:返回fn
}

const myFn = name()
const myFn2 = name()
myFn() // 第一次执行 输出1
myFn() // 第一次执行 输出2
myFn2() // 第一次执行 输出1
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么会这样呢?问题其实就出在 name还是那个name,但是a已经不是从前那个a了。

再回到react的示例中,我们可以简单理解为在调用完setCount之后,再次执行MyComponent函数去重新渲染视图,这个时候count的初值就已经是1了(虽然react底层原理并不是这样,但这里可以这样简易理解)。

这个时候再去渲染视图,直接就是拿着1去渲染了,所以视图已经变了,那为什么打印里没有变成1呢?

这里有需要稍微拐个弯,请注意,我们的打印是在第一次调用MyComponent时创建的handleClick函数里,而count变为1是在第二次调用MyComponent函数里。这可不就只能拿到旧值吗?

现在已经真相大白了,因为setState之后会重新调用MyComponent,而最新状态只能在重新调用MyComponent后的上下文中获取,所以我们引用的一直都是旧的状态。

那么该如何去解决这个问题呢?

# 解决方案

如果是在设置状态的时候需要用到最新值。我们可以通过传入函数的方式

setCount(prev=>prev+1)
1

prev就是当前状态的最新值。

方案2:通过useRef始终保持最新引用

const [count,setCount] = useState(0)
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const timer = setInterval(() => {
    console.log(countRef.current); // 最新值
  }, 1000);
  
  return () => clearInterval(timer);
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13

这里我们通过useEffect去监听count的变化,并且实时的赋值给ref,这样就可以保证Ref的值始终是最新值。 但是这里也会有一个小小的问题由于setCount是异步执行的,所以会导致如果调用完setCount后立马访问Ref拿到的也是旧值,这是因为setCount的异步执行导致useEffect也没办法立马触发。 所以ref的值没有立即更新。通常这种情况需要用定时器包裹一下就可以拿到最新的ref值了(这里里用了js宏任务和微任务的执行顺序原理),像下面这样

const [count,setCount] = useState(0)

const countRef = useRef(count);

useEffect(() => {
  countRef.current = count;
}, [count]);

setCount(1)

setTimeout(()=>{
    console.log(countRef.current) // 这里拿到的就是最新值了
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14

那如果每次都需要写一个useEffect去监听并且还要创建ref去接受的话会比较麻烦,所以这里我们可以封装一个简单的hooks来整合这一系列操作

function useLatestState(initialState) {
  const [state, setState] = useState(initialState);
  const ref = useRef(state);

  const setLatestState = useCallback((newState) => {
    ref.current = typeof newState === 'function' 
      ? newState(ref.current) 
      : newState;
    setState(ref.current);
  }, []);

  return [state, setLatestState, ref];
}

// 使用
const [count, setCount, countRef] = useLatestState(0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 结语

其实没有什么特别复杂的东西,这里主要需要弄清楚react的异步更新,以及重新渲染。然后加上js的闭包知识就能很清晰的知道这个问题了。 react没有像Vue那样做很多上层封装,所以一些js 的问题都原原本本的暴露了出来。但这并不是react的bug,而是强调开发者需要更强的js代码书写能力。

编辑 (opens new window)
上次更新: 2025/12/03, 17:26:13
react路由切换过度效果

← react路由切换过度效果

最近更新
01
react路由切换过度效果
11-08
02
小程序实现全局状态管理
07-09
03
前端检测更新,自动刷新网页
06-09
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式