setState后React如何处理更新

当组件调用 setState() 更新组件的状态时,React 并不会立即更新组件,而是把这个更新操作放到一个更新队列里(undater)。这就像是在餐厅点菜,服务员先记下顾客的点单,但不是每点一道菜就去厨房一次,而是等顾客点完了所有菜,才把整个订单一起送到厨房。

在某些时候,特别是当组件在很短的时间内多次调用 setState() 时,React 会将这些单独的更新合并成一个大的更新。这样做的好处是效率高:React 可以减少不必要的计算和渲染工作,因为它不需要为每次 setState() 都重新渲染整个组件,只需要根据最终的状态重新渲染一次即可。

React 会选择合适的时机进行这个”厨房制作”过程——也就是更新组件的状态并重新渲染。在这个过程中,它会看看有哪些更新,把它们合并成最终的状态,然后一次性更新组件,而不是一点一点来。

最后,就像服务员将厨房做好的菜端给顾客,React 也会用这个最新的状态来渲染组件的界面,确保看到的内容是最新的。这整个过程让React应用运行得更快,也更流畅。

setState后React依据什么更新内容

React 的更新队列中的内容何时更新,主要由以下因素决定:

  1. 事件循环: JavaScript 是单线程执行的,而且使用事件循环机制来处理异步事件。React 利用这个机制来决定何时更新状态。通常,在事件循环的一个周期中,React 会将多个 setState() 调用批量处理。

  2. 批量更新: 默认情况下,React 在处理诸如用户交互、生命周期方法或者其自身的事件处理(比如表单onChange事件)时,会自动将这些 setState() 调用合并成一个批量更新。

  3. 优先级: React 18 引入了并发特性,这意味着 React 可以为不同的更新分配优先级。例如,用户的交互(如点击)可能会触发比数据获取回调中的更新更高优先级的更新。

  4. React 调度器(Scheduler): React 内部有一个调度器,它根据优先级和其他因素来决定何时处理更新队列。这个调度器可以决定立即更新,也可以稍后更新,使得更高优先级的任务可以先执行。

  5. 并发模式: 如果你的应用启用了 React 的并发模式,React 会更加智能地安排何时进行更新。并发模式允许React打断更新过程,来先执行更重要的任务,例如用户的输入。

  6. 异步操作: 异步操作(如 setTimeoutsetInterval 或网络请求回调)中的 setState() 不会自动批处理,但在React 18中,自动批处理也扩展到了这些场景。

  7. 强制更新: 如果开发者使用了 forceUpdate() 方法,React 会绕过状态更新的合并和调度,强制组件重新渲染。

综上所述,React 的更新队列何时被处理,取决于当前执行环境(同步或异步)、更新的优先级、是否启用并发特性以及内部调度器的策略。React 的设计目标是尽可能地在不牺牲用户体验的情况下提高性能,因此更新策略会尽量减少不必要的渲染和计算,同时响应重要的用户交互。

理解React批处理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* @Description:
* @Author: xiuji
* @Date: 2023-12-21 14:46:49
* @LastEditTime: 2024-01-03 15:17:00
* @LastEditors: Do not edit
*/
import React, { Component } from 'react';

class Demo extends Component {
state = {
x: 10,
y: 20,
z: 30,
}

handle = () => {
this.setState({
x:100
})
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
})
console.log(this.state.y); // 此时的this.state.y还是20
setTimeout(() => {
this.setState({
z:300
})
console.log(this.state); // {x: 100, y: 200, z: 30},此时的this.state.z还是30
},1000)
}

render() {
console.log('视图渲染:RENDER');
let { x, y, z } = this.state;
return (
<div>
x - {x} <br />
y - {y} <br />
z - {z} <br />
<button onClick={this.handle}>change</button>
</div>
);
}
}

export default Demo;

上述组件在惦记change按钮后会执行两次render方法,将产生两个更新队列

更新队列1:

1
2
3
4
5
6
7
8
this.setState({
x:100
}) // 将state中x的状态修改为100
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
}) // 将state中y的状态修改为100
console.log(this.state.y); // 此时的this.state.y还是20

第一和第二次 setState() 调用位于同一个执行上下文中。因为 React 的批处理机制,这两个 setState() 调用将被合并,而且由于 setState() 是异步的,console.log 会在 React 更新状态之前执行。这就是为什么会看到 this.state.xthis.state.y 打印出它们旧的值

更新队列2:

1
2
3
4
this.setState({
z:300
}) // 将state中z的状态修改为300
console.log(this.state); // {x: 100, y: 200, z: 30},此时的this.state.z还是30

第三次 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* @Description:
* @Author: xiuji
* @Date: 2023-12-21 14:46:49
* @LastEditTime: 2024-01-08 16:25:54
* @LastEditors: Do not edit
*/
import React, { Component } from 'react';
class Demo extends Component {
state = {
x: 10,
y: 20,
z: 0,
}

handle = () => {
this.setState({
x:100
})
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
})
console.log(this.state.y); // 此时的this.state.y还是20
this.setState({
z: this.state.x + this.state.y
})
console.log(this.state) // {x: 100, y: 200, z: 30}
}

render() {
console.log('视图渲染:RENDER');
let { x, y, z } = this.state;
return (
<div>
x - {x} <br />
y - {y} <br />
z - {z} <br />
<button onClick={this.handle}>change</button>
</div>
);
}
}

export default Demo;

第一个和第二个 setState 调用被合并,而第三个 setState 调用是基于合并前的状态(即初始状态,其中 this.state.x 是 10,this.state.y 是 20)进行计算的。所以当第三个 setState 被执行时,它使用的 xy 的值还没有更新,因此 z 被设置成了 30。

尽管 setState 调用是异步的,但是在所有的 setState 调用完成后,React会触发一次渲染,此时的 xy 已经是更新后的值(100 和 200),但由于 z 已经在之前被设置成了 30,所以渲染结果显示 z 为 30。

下面是 handle 函数的调用栈及其对应的状态:

  1. this.setState({ x:100 }) 被调用,但状态更新还未执行,this.state.x 仍然是 10。
  2. 第一个 console.log(this.state.x) 执行,输出 10。
  3. this.setState({ y:200 }) 被调用,但状态更新还未执行,this.state.y 仍然是 20。
  4. 第二个 console.log(this.state.y) 执行,输出 20。
  5. this.setState({ z: this.state.x + this.state.y }) 被调用,由于此时状态还未更新,所以 z 被设置成了 10 + 20,即 30。
  6. 第三个 console.log(this.state) 执行,输出 {x: 10, y: 20, z: 30},因为状态的批量更新还没有被执行。

使用定时器完成需求(不可取、不建议)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
handle = () => {
this.setState({
x:100
})
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
})
console.log(this.state.y); // 此时的this.state.y还是20
this.setState({
z: this.state.x + this.state.y
})
console.log(this.state) // {x: 10, y: 20, z: 30}
setTimeout(() => {
this.setState({
z: this.state.x + this.state.y
})
console.log(this.state); // {x: 100, y: 200, z: 30},此时的this.state.z还是30
},1000)
}

本质是将状态更新放在两个更新队列中,x、y处于一个队列,z处于一个队列,通过时间差完成需求,render方法会更新两次,实际开发不可取

优化:使用flushSync同步更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
handle = () => {
this.setState({
x:100
})
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
})
console.log(this.state.y); // 此时的this.state.y还是20
this.setState({
z: this.state.x + this.state.y
})
flushSync(); // 同步更新
this.setState({
z: this.state.x + this.state.y
})
console.log(this.state) // {x: 100, y: 200, z: 300}
}

设置完新的y值后使用flushSync强制React立即重新渲染组件,而不是等到批处理完成,同步更新后x、y的值已经更新为设置的新值。这个过程会中断正在进行的批处理,并会导致组件的额外一次渲染。

优化+1: 将函数传递给 setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
handle = () => {
this.setState({
x:100
})
console.log(this.state.x); // 此时的this.state.x还是10
this.setState({
y:200
})
console.log(this.state.y); // 此时的this.state.y还是20
this.setState({
z: this.state.x + this.state.y
})
flushSync(() => {
this.setState(prevState => {
return {
z: prevState.x + prevState.y
}
})
}); // 同步更新
console.log(this.state) // {x: 10, y: 20, z: 30}
}

可以将函数传递给 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文档来获取当前版本的行为细节和最佳实践。