深入浅出 setState

读完本文你将知道:

  • setState 不会立即改变 React 组件中 state 的值 (异步)
    • setState异步的,包括在setTimeout里也是异步的 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)
  • setState 通过引发一次组件的更新过程来引发重新绘制
  • 多次 setState 函数调用产生的效果会合并(批处理)

setState 的特性——批处理

如果在同一周期多次调用 setState ,后调用的 setState 将覆盖先调用的 setState 的值,例如:

1
2
3
4
5
// state.count === 0
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// state.count === 1

执行 3+1,但最后只加了 1 次;若在 setTimeout 中多次调用,结果也一样

1
2
3
4
5
6
7
// state.count === 0
setTimeout(() => {
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
}, 0)
// state.count === 1

因为这样的操作相当于 Object.assign,最后一个会把前面的都给覆盖

1
2
3
4
5
6
7
// 相当于
Object.assign(
state,
{count: state.count + 1},
{count: state.count + 1},
{count: state.count + 1},
)

同一个时期,多次调用,会合并

函数组件类组件在同一时期,多次调用setState,会合并。

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
const DemoState = (props) => {
let [number, setNumber] = useState(0);

const add = () => {
setNumber(number+1);
console.log(number); // 0

setNumber(number+1);
console.log(number); // 0

setNumber(number+1);
console.log(number); // 0
}

return (
<div>
<span>{ number }</span>
<button onClick={() => { add() }} >点击加 1</button>
</div>
)
}

// 0
// 0
// 0

// 页面展示 number 为 1

若上面的 3+1 都放在 setTimeout 执行,也是会合并的,并且仍然是异步 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)

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
const DemoState = (props) => {
let [number, setNumber] = useState(0);

const add = () => {

setTimeout(() => {
setNumber(number+1);
console.log(number); // 0

setNumber(number+1);
console.log(number); // 0

setNumber(number+1);
console.log(number); // 0
}, 0)
}
return (<div>
<span>{ number }</span>
<button onClick={() => { add() }} >点击加 1</button>
</div>)
}
// 0
// 0
// 0

// 页面展示 number 为 1

函数组件在不同时期,会合并

函数组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 调用,另一个在 setTimeout 调用。最后合并了,只调用了 1 次 +1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const DemoState = (props) => {
let [number, setNumber] = useState(0);

const add = () => {
setNumber(number+1);
console.log(number); // 0

setTimeout(() => {
setNumber(number+1);
console.log(number); // 0
}, 0)
}
return (<div>
<span>{ number }</span>
<button onClick={() => { add() }} >点击加 1</button>
</div>)
}

// 0
// 0

// 页面展示 number 为 1

类组件在不同时期,不会合并

类组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 调用,另一个在 setTimeout 调用。最后没合并2 次 +1 都被调用

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
class DemoState2 extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
}
}

add = () => {
this.setState({number: this.state.number + 1});
console.log(this.state.number); // 0

setTimeout(() => {
this.setState({number: this.state.number + 1});
console.log(this.state.number); // 1
}, 0)
}

render() {
return (
<div>
<span>{ this.state.number }</span>
<button onClick={this.add} >点击加 1</button>
</div>
)
}
}

// 0
// 1

// 页面展示 number 为 2

当前测试 react 版本:18.1.0

这可能是 react 的一个 bug,看看后面会不会在函数组件和类组件中保持一致。

下面看看由批处理引发的问题:

问题 1:连续使用 setState,为什么不能实时改变

1
2
3
4
5
state.count = 0;
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// state.count === 1,不是 3

因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,如下所示:

1
2
3
4
5
state.count = 0;
this.setState({count: state.count + 2});
this.setState({count: state.count + 3});
this.setState({count: state.count + 4});
// state.count === 4

问题 2:为什么要 setState,而不是直接 this.state.xx = oo?

  1. setState 不仅仅修改了 this.state 的值,更重要的是它会触发 React更新机制,会进行 diff,然后将 patch 部分更新到真实 dom
  2. 如果直接 this.state.xx = oo 的话,state 的值确实会改,但是它不会驱动 React 重渲染,不会触发后续生命周期,如 shouldComponentUpdaterender 等一系列函数的调用。
  3. 对于批处理,多次setState 只产生一次重新渲染,将对 Virtual DOMDOM 树操作降到最小,用于提高性能

问题 3:那为什么会出现异步的情况呢?(为什么这么设计?)

因为性能优化。假如每次 setState 都要更新数据,更新过程就要走五个生命周期,走完一轮生命周期再拿 render 函数的结果去做 diff 对比和更新真实 DOM,会很耗时间。所以将每次调用都放一起做一次性处理,能降低对 DOM 的操作,提高应用性能

问题 4:那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?

  • setState(stateChange[, callback])
  • setState((state, props) => stateChange[, callback])
1
2
3
4
5
6
7
8
9
10
11
12
13
onHandleClick() {
this.setState(
{count: this.state.count + 1,},
() => {
console.log("点击之后的回调", this.state.count); // 最新值
}
);
}

this.setState(state => {
console.log("函数模式", state.count);
return { count: state.count + 1 };
});