
本文探讨react 18中,当多个独立事件(如onmousedown和onfocus)在短时间内触发状态更新时,setstate回调函数可能被多次执行的现象。我们将分析react的事件批处理机制,特别是其不跨越不同意图事件的特性,以及如何通过丢弃陈旧结果来确保最终状态的一致性,强调updater函数纯粹性的重要性。
在React应用开发中,我们通常期望setState的更新函数(updater function)在一次状态更新周期中只被执行一次。然而,在特定场景下,尤其是在React 18的自动批处理机制下,当多个“有意图的事件”(intentional events)在短时间内连续触发状态更新时,我们可能会观察到setState的更新函数被多次执行,即使没有开启严格模式(Strict Mode)。这种行为可能导致开发者困惑,但实际上是React内部机制为了确保状态一致性而采取的策略。
场景复现与现象分析
考虑以下React组件代码,其中包含两个状态state和state2,并通过useEffect和onFocus事件处理器触发setState更新:
import React, { useState, useEffect, useRef } from "react";import "./styles.css";function App() { const [state, setState] = useState([]); const [state2, setState2] = useState(0); // 用于记录渲染迭代次数 const render = useRef(0); render.current++; useEffect(() => { if (state2) { console.log(render.current, performance.now(), "effect"); setState(s => { console.log(render.current, performance.now(), "effect setState", s); return [...s, "effect"]; }); } }, [state2]); return ( { console.log(render.current, performance.now(), "mousedown"); setState2(1); }} onFocus={() => { console.log(render.current, performance.now(), "focus"); setState(s => { console.log(render.current, performance.now(), "focus setState", s); return [...s, "focus"]; }); }} /> );}
当用户点击元素时,onMouseDown事件会先于onFocus事件触发。我们期望的控制台输出可能是:
effectfocuseffect setState []focus setState ['effect']
然而,实际的控制台输出(可能略有不同,但核心行为一致)会是:
1 2971 "mousedown" 2 2974 "effect" 2 2978 "focus" 3 2978 "focus setState" [] // 第一次执行,基于旧的state4 2982 "effect setState" []4 2982 "focus setState" (1) ["effect"] // 第二次执行,基于更新后的state
从上述输出中可以看到,”focus setState”的日志出现了两次,其中第一次的s是空数组[],而第二次的s是[‘effect’]。这表明onFocus中的setState更新函数被执行了两次,并且第二次执行时接收到了由useEffect更新后的正确状态。
React的批处理机制与事件边界
要理解这一现象,关键在于React 18的自动批处理(Automatic Batching)机制以及其对“有意图的事件”的处理。
自动批处理: React 18默认会在事件处理器、useEffect、定时器等回调中将多个setState调用合并成一次重新渲染,以优化性能。跨事件批处理的限制: 尽管React会自动批处理,但它不会跨越多个“有意图的事件”进行批处理。onMouseDown和onFocus被React视为两个独立的、有意图的用户交互事件。这意味着,React会为每个事件独立地处理其内部触发的状态更新队列。
在我们的例子中:
onMouseDown触发: setState2(1)被调用。由于state2改变,这将触发一次重新渲染,并且useEffect的依赖项[state2]也会随之更新。useEffect触发: state2从0变为1,useEffect回调执行,并调用setState(s => […s, “effect”])。onFocus触发: 几乎同时,onFocus事件触发,调用setState(s => […s, “focus”])。
由于onMouseDown和onFocus是独立的事件,React可能在处理onFocus事件的更新队列时,state2的更新及其导致的useEffect中的setState尚未完全提交到DOM。
详细执行流程分析
结合日志中的渲染迭代次数和时间戳,我们可以更清晰地追踪执行流程:
渲染迭代 1: 初始渲染。1 2971 “mousedown”: onMouseDown事件触发,setState2(1)被调用。这会标记一次新的渲染。2 2974 “effect”: React进入新的渲染周期(渲染迭代 2)。此时state2已更新,useEffect回调执行。2 2978 “focus”: 在渲染迭代 2 中,onFocus事件触发。3 2978 “focus setState” []: React开始处理onFocus事件内部的setState。此时,它可能基于前一个已提交的渲染状态(即state仍为[])来计算state。因此,focus setState的updater函数被调用,接收到的s是[]。这会产生一个待处理的更新队列。4 2982 “effect setState” []: React继续处理其他更新。在某个时刻,useEffect中setState(s => […s, “effect”])的updater函数被调用。它也可能基于[]来计算,返回[‘effect’]。4 2982 “focus setState” (1) [“effect”]: 由于React检测到在渲染迭代 3 中onFocus的setState是基于一个陈旧的状态进行计算的(因为effect setState已经改变了状态),为了确保最终状态的正确性,React会丢弃渲染迭代 3 中focus setState的计算结果,并重新执行focus setState的updater函数。这次,它会接收到由effect setState更新后的[‘effect’]作为s,最终返回[‘effect’, ‘focus’]。
这种行为与严格模式下setState updater函数被执行两次(并丢弃第二次结果)有相似之处,但其根本原因不同。严格模式是为了帮助开发者发现不纯的副作用,而这里则是React为了在复杂的并发更新和事件边界下,确保最终状态的一致性,通过重新执行updater函数来基于最新的状态进行计算。
启示与最佳实践
最终状态的一致性: 尽管setState的updater函数可能被多次调用,但React会确保最终提交到DOM的状态是正确的。在上述例子中,最终的state将是[‘effect’, ‘focus’],符合我们的预期。Updater函数必须是纯函数: 这一现象再次强调了setState的updater函数(如s => {…})必须是纯函数的原则。它不应该有任何副作用,不应该修改外部变量,并且对于相同的输入,必须始终返回相同的输出。如果updater函数包含副作用,这些副作用可能会被意外地多次触发,导致难以调试的bug。避免在Updater函数中进行非幂等操作: 如果updater函数中包含像网络请求、DOM操作或其他非幂等(多次执行会产生不同结果)的逻辑,那么多次执行会导致不可预测的行为。这些操作应该放在useEffect或其他生命周期方法中。理解React的内部机制: 了解React如何处理并发更新和事件批处理,有助于我们更好地预测组件行为,并编写更健壮的代码。
总之,当你在React 18中观察到setState的updater函数被多次执行时,不必惊慌。这通常是React为了保证状态一致性而采取的内部优化策略,尤其是在多个独立事件触发更新的场景下。关键在于始终遵循React的函数式编程范式,确保updater函数的纯粹性。
以上就是深入理解React setState回调的多次执行:事件批处理与状态一致性的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1542082.html
微信扫一扫
支付宝扫一扫