React中setState在同步与异步回调中重渲染时机差异的疑问
疑问点:第一个示例中,useEffect同步代码里调用setAllMemes时,组件会等待该useEffect内所有代码执行完毕后才触发重渲染;第二个示例中,fetch的then异步回调里调用setAllMemes时,组件会在回调代码未执行完就触发重渲染,为何会出现这种差异?
第一个代码示例
import React from "react" export default function Meme() { console.log("react render") const [allMemes, setAllMemes] = React.useState([]) console.log("allMemes outside the use effect" , allMemes) React.useEffect(() => { console.log("before setAllMemes") setAllMemes(["string1","string2"]); console.log("after setAllMemes"); console.log("allMemes inside the use effect" , allMemes); }, []) return ( <main> </main> ) }
控制台输出
›react render ›allMemes outside the use effect,[] ›before setAllMemes ›after setAllMemes ›allMemes inside the use effect,[] ›react render ›allMemes outside the use effect,["string1", "string2"]
第二个代码示例
import React from "react" export default function Meme() { console.log("react render") const [allMemes, setAllMemes] = React.useState([]) console.log("allMemes outside the use effect" , allMemes) React.useEffect(() => { fetch("https://api.imgflip.com/get_memes") .then(res => res.json()) .then(data => { console.log("before setAllMemes") setAllMemes(data.data.memes); console.log("after setAllMemes"); }) console.log("allMemes inside the use effect" , allMemes); }, []) return ( <main> </main> ) }
控制台输出
›react render ›allMemes outside the use effect,[] ›allMemes inside the use effect,[] ›before setAllMemes ›react render ›allMemes outside the use effect [array of object from the api] ›after setAllMemes
原因解释
核心差异来自JavaScript事件循环机制和React状态更新的批量处理策略:
同步代码场景(第一个示例)
useEffect的回调是同步执行的,在这个同步任务流中,React会开启批量更新模式:调用setAllMemes时,React不会立刻触发重渲染,而是将状态更新任务加入队列,等当前同步代码(整个useEffect回调)执行完毕后,再一次性处理队列中的更新并触发重渲染。这就是为什么after setAllMemes和allMemes inside the use effect会先打印,之后才出现第二次react render。异步回调场景(第二个示例)
fetch的then回调属于微任务,会在当前同步任务(useEffect内的console.log("allMemes inside the use effect" , allMemes);)执行完成后,才进入事件循环队列等待执行。而React在Promise、setTimeout这类异步回调中,默认不会启用批量更新(React 18之前的行为,若在React 18的并发模式下会有自动批量更新,但本次输出符合旧行为特征)。当在then里调用setAllMemes时,React会立即触发重渲染流程,此时then回调里的console.log("after setAllMemes");还未执行——因为重渲染任务会插入事件循环,优先于剩余的回调代码执行,所以会先看到react render,再看到after setAllMemes。
另外补充:useEffect里打印的allMemes始终是初始值,是因为useEffect回调捕获了组件首次渲染时的状态闭包,即便调用了setState,当前回调内的状态变量不会立刻更新。
内容的提问来源于stack exchange,提问作者mohamed azaiez

