React onClick事件会触发多次重渲染吗?
核心规则:React批量更新的适用范围
React的批量更新(把多个状态更新合并为一次重渲染)只在React可控的同步执行上下文里生效,包括:
- 原生合成事件回调(比如
onClick、onChange这类) - React生命周期函数
useEffect等Hooks的同步回调代码
在这些场景下,所有同步执行的setState会被塞进更新队列,等当前上下文的代码全部跑完,才会触发一次重渲染。
你的代码为什么触发两次重渲染?
拆解handleClick的执行流程就清楚了:
同步执行阶段(事件处理函数内):
执行setPending((p) => p + 1)时,这个状态更新被加入React的更新队列。此时handleClick只是注册了setTimeout回调,并没有执行它,所以React会等handleClick完全执行完,才触发第一次重渲染,更新pending的值。异步执行阶段(setTimeout回调):
3秒后浏览器执行setTimeout的回调,这个回调属于浏览器的宏任务,已经脱离了React的同步事件上下文。里面的setCompleted和setPending两个状态更新,在React 18+里会被自动合并成一次重渲染;如果是React 17及更早版本,可能触发两次,但你的情况里应该是合并成了第二次重渲染——所以总共会看到两次打印。
为什么和文档描述不矛盾?
文档里的这句话:
UI 不会更新,直到你的事件处理函数及其中的所有代码执行完成。
这里的“事件处理函数及其中的所有代码”特指同步执行的代码。setTimeout的回调是异步任务,会在事件处理函数执行完毕、浏览器走完当前事件循环后才运行,不属于事件处理函数的同步执行范围。所以这个回调执行时,相当于开启了一个新的更新周期,自然会触发新的重渲染。
额外验证:同步场景下的批量更新
如果把setTimeout里的两个状态更新放到同步的事件回调里,比如:
function handleClick() { setPending((p) => p + 1); setCompleted((c) => c + 1); setPending((p) => p - 1); }
这三个状态更新会被合并成一次重渲染,pending最终回到初始值,completed加1,只会触发一次MyComponent rendered!的打印——这才是文档描述的“事件处理完成后更新UI”的典型场景。
内容的提问来源于stack exchange,提问作者qhuboo

