普通setState与函数式setState的异步场景表现差异解析
代码片段 #1
import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import "./styles.css"; function DelayedCount() { const [count, setCount] = useState(0); useEffect(() => { console.log("updating count...", count) }, [count]) function handleClickAsync() { setTimeout(function delay() { setCount(prevCount => prevCount + 1); }, 3000); } return ( <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div> ); }
代码片段 #2
import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom"; import "./styles.css"; function DelayedCount() { const [count, setCount] = useState(0); useEffect(() => { console.log("updating count...", count) }, [count]) function handleClickAsync() { setTimeout(function delay() { setCount(count + 1); //UPDATED THIS PART OF CODE!!! }, 3000); } return ( <div> {count} <button onClick={handleClickAsync}>Increase async</button> </div> ); }
问题
快速点击按钮3次后,代码片段1中useEffect内的console.log执行3次;代码片段2中将setCount改为普通状态更新后,点击3次useEffect仅执行一次。为何React在两种场景下的状态更新表现不同?
原因解析
核心差异来自状态更新的依赖逻辑和闭包对变量的捕获特性:
代码片段1:函数式更新
每次点击按钮时,setTimeout的回调会捕获到setCount函数(React保证这个函数的引用始终稳定)。3秒后回调执行时,函数式更新会直接从React内部获取当前最新的count值,每次更新都是基于最新状态加1。三次点击触发的三个异步回调,各自执行时都会触发一次独立的状态变更,每次变更后count值变化,都会触发useEffect执行,所以最终console.log会跑3次。代码片段2:直接值更新
每次点击按钮时,setTimeout的回调会捕获到点击瞬间的count值(这是闭包的特性)。假设三次点击时count都是0,三个回调捕获的count值就都是0。3秒后三个回调依次执行,每次都试图把count设为0+1=1——这三次更新的目标值完全相同,React会自动合并这类重复的状态更新,最终只触发一次状态变更,useEffect自然也就只执行一次。
另外补充:在setTimeout这类React无法直接控制的异步回调中,React不会开启批量更新(批量更新仅在React可控的同步事件流,比如按钮点击的同步回调中生效),所以代码片段1的三次更新是依次触发的,而代码片段2的三次更新因为目标值重复被合并。
内容的提问来源于stack exchange,提问作者D_S_X

