React-深入理解setState
setState后React如何处理更新
当组件调用 setState()
更新组件的状态时,React 并不会立即更新组件,而是把这个更新操作放到一个更新队列里(undater)。这就像是在餐厅点菜,服务员先记下顾客的点单,但不是每点一道菜就去厨房一次,而是等顾客点完了所有菜,才把整个订单一起送到厨房。
在某些时候,特别是当组件在很短的时间内多次调用 setState()
时,React 会将这些单独的更新合并成一个大的更新。这样做的好处是效率高:React 可以减少不必要的计算和渲染工作,因为它不需要为每次 setState()
都重新渲染整个组件,只需要根据最终的状态重新渲染一次即可。
React 会选择合适的时机进行这个”厨房制作”过程——也就是更新组件的状态并重新渲染。在这个过程中,它会看看有哪些更新,把它们合并成最终的状态,然后一次性更新组件,而不是一点一点来。
最后,就像服务员将厨房做好的菜端给顾客,React 也会用这个最新的状态来渲染组件的界面,确保看到的内容是最新的。这整个过程让React应用运行得更快,也更流畅。
setState后React依据什么更新内容
React 的更新队列中的内容何时更新,主要由以下因素决定:
事件循环: JavaScript 是单线程执行的,而且使用事件循环机制来处理异步事件。React 利用这个机制来决定何时更新状态。通常,在事件循环的一个周期中,React 会将多个
setState()
调用批量处理。批量更新: 默认情况下,React 在处理诸如用户交互、生命周期方法或者其自身的事件处理(比如表单onChange事件)时,会自动将这些
setState()
调用合并成一个批量更新。优先级: React 18 引入了并发特性,这意味着 React 可以为不同的更新分配优先级。例如,用户的交互(如点击)可能会触发比数据获取回调中的更新更高优先级的更新。
React 调度器(Scheduler): React 内部有一个调度器,它根据优先级和其他因素来决定何时处理更新队列。这个调度器可以决定立即更新,也可以稍后更新,使得更高优先级的任务可以先执行。
并发模式: 如果你的应用启用了 React 的并发模式,React 会更加智能地安排何时进行更新。并发模式允许React打断更新过程,来先执行更重要的任务,例如用户的输入。
异步操作: 异步操作(如
setTimeout
、setInterval
或网络请求回调)中的setState()
不会自动批处理,但在React 18中,自动批处理也扩展到了这些场景。强制更新: 如果开发者使用了
forceUpdate()
方法,React 会绕过状态更新的合并和调度,强制组件重新渲染。
综上所述,React 的更新队列何时被处理,取决于当前执行环境(同步或异步)、更新的优先级、是否启用并发特性以及内部调度器的策略。React 的设计目标是尽可能地在不牺牲用户体验的情况下提高性能,因此更新策略会尽量减少不必要的渲染和计算,同时响应重要的用户交互。
理解React批处理操作
1 | /* |
上述组件在惦记change按钮后会执行两次render
方法,将产生两个更新队列
更新队列1:
1 | this.setState({ |
第一和第二次 setState()
调用位于同一个执行上下文中。因为 React 的批处理机制,这两个 setState()
调用将被合并,而且由于 setState()
是异步的,console.log
会在 React 更新状态之前执行。这就是为什么会看到 this.state.x
和 this.state.y
打印出它们旧的值
更新队列2:
1 | this.setState({ |
第三次 setState()
被放在 setTimeout
回调里。在 JavaScript 中,setTimeout
(和其他的宏任务)将在当前执行栈清空后的某个时刻执行。在这个时候,之前的批处理已经完成,React 已经渲染了之前的更新。因此,当第三个 setState()
被调用时,它将创建一个新的更新队列,该队列只包含一个更新。这就是为什么 this.state.x、this.state.y
已经改变,而this.state.z
仍然为30
flushSync立即更新DOM
在 React 16 中,flushSync
这个 API 并不存在。而在 React 18,它是新引入的 API,作为一个工具,以供开发者在确实需要的时候,使用同步的方式去处理更新。
在 React 18 中,flushSync
是一个可以用来强制同步刷新状态更新和 DOM 更新的函数。如果在 setState
之后调用 flushSync
,React 会立刻停止其批处理和异步调度行为,同步地应用所有待处理的状态更新和渲染工作。
在某些情况下,你可能需要确保某些更新是立即执行的,例如,在处理某些特定的用户交互或动画时,这时候 flushSync
就变得十分有用。但是,需要注意的是,过度使用 flushSync
会降低应用性能,因为它绕过了 React 的优化机制,比如批处理和异步渲染。
需求:state中z的值需要通过修改后的x、y值相加获取
1 | /* |
第一个和第二个 setState
调用被合并,而第三个 setState
调用是基于合并前的状态(即初始状态,其中 this.state.x
是 10,this.state.y
是 20)进行计算的。所以当第三个 setState
被执行时,它使用的 x
和 y
的值还没有更新,因此 z
被设置成了 30。
尽管 setState
调用是异步的,但是在所有的 setState
调用完成后,React会触发一次渲染,此时的 x
和 y
已经是更新后的值(100 和 200),但由于 z
已经在之前被设置成了 30,所以渲染结果显示 z
为 30。
下面是 handle
函数的调用栈及其对应的状态:
this.setState({ x:100 })
被调用,但状态更新还未执行,this.state.x
仍然是 10。- 第一个
console.log(this.state.x)
执行,输出 10。 this.setState({ y:200 })
被调用,但状态更新还未执行,this.state.y
仍然是 20。- 第二个
console.log(this.state.y)
执行,输出 20。 this.setState({ z: this.state.x + this.state.y })
被调用,由于此时状态还未更新,所以z
被设置成了10 + 20
,即 30。- 第三个
console.log(this.state)
执行,输出{x: 10, y: 20, z: 30}
,因为状态的批量更新还没有被执行。
使用定时器完成需求(不可取、不建议)
1 | handle = () => { |
本质是将状态更新放在两个更新队列中,x、y处于一个队列,z处于一个队列,通过时间差完成需求,render方法会更新两次,实际开发不可取
优化:使用flushSync同步更新
1 | handle = () => { |
设置完新的y值后使用flushSync
强制React立即重新渲染组件,而不是等到批处理完成,同步更新后x、y的值已经更新为设置的新值。这个过程会中断正在进行的批处理,并会导致组件的额外一次渲染。
优化+1: 将函数传递给 setState
1 | handle = () => { |
可以将函数传递给 setState
。它允许你根据先前的 state 来更新 state,此时点击按钮后页面只会渲染一次。
使用 flushSync
是不常见的行为,并且可能损伤应用程序的性能。详情见官方文档
setState在React18与React16中的区别
在React中,setState
函数的行为在React 16和React 18中有所不同,特别是关于它的同步和异步行为以及批量更新的处理。
React 16
在React 16及以前的版本中,setState
主要表现为异步。在事件处理、生命周期方法或者 setTimeout
/setInterval
回调中调用时,多个 setState
调用会被批量处理,以减少不必要的渲染和提高性能。这意味着即使你连续多次调用 setState
,React也会将这些更新合并然后执行一次渲染。
但是,setState
在一些情况下也会是同步的,比如在setTimeout或setInterval回调中,以及在原生事件处理中(也就是你直接操作DOM绑定的事件处理程序中)。
React 18
React 18引入了并发(Concurrent)模式和新的批量更新机制。在React 18中,默认情况下,无论 setState
是在事件处理、生命周期方法还是异步操作中调用的,React可能会根据需要将多个状态更新批量处理。这意味着React更加智能地管理状态更新,以实现更好的并发性能和响应能力。批量更新不仅仅适用于同步事件,也适用于如Promise回调这样的异步代码。
flushSync
函数在React 18中被引入,以允许开发者在必要时强制React同步执行 setState
调用。flushSync
可以用于确保某个特定状态更新被立即应用,而不是等待批处理。
总结
- React 16中,
setState
通常在事件处理函数中是异步的,会被批量处理更新。 - React 18中,批量更新的机制被改进,以更好地利用并发特性,提供更平滑的用户体验,并且在更多的场景中进行批量更新。
flushSync
在React 18中被引入,允许开发者在特定情况下强制同步更新状态。
了解这些区别非常重要,因为它们会影响到你如何编写代码以及你的应用的性能和行为。随着React的不断发展,最好的做法是查阅最新的React文档来获取当前版本的行为细节和最佳实践。