React提高01 SetState的执行时机

React的setState并不保证是同步执行的,但是也不一定就是异步执行的,准确的说是利用了队列来模拟异步执行,并没有用到任务的异步API这篇文章分析了setState的执行机制,帮助我理解setState的执行时机有很大帮助。

执行时机

先放下结论:

  1. setState的实现并没有涉及到任何的异步API
  2. 真正更新组件state的是flushBatchedUpdates函数,而setState不一定会调用这个函数,有可能多次setState调用一次这个函数
  3. setState会不会立刻更新state取决于调用setState时是不是已经处于批量更新事务中。
  4. 组件的生命周期函数和绑定的事件回调函数都是在批量更新事务中执行的。

延迟调用

componentDidMount生命周期中执行setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log('第一次 ', this.state.val);
}

render() {
return (
<div>
{this.state.val}
</div>
);
}
}

执行的结果是什么?

控制台会打印第一次 0,页面上显示1。这是因为componentDidMount生命周期是批量更新事务,在批量更新事务中调用setState不会立即执行,而是放到队列中等待批量更新事务结束后统一执行。

所以生命周期中打印出来的是0,而render中上一个生命周期的更新已经结束,所以页面显示1

同理,在两个顺序的生命周期中调用setState,第二个生命周期的state就是已经更新过的state

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

componentWillMount() {
this.setState({ val: this.state.val + 1 });
console.log('第一次 ', this.state.val);
}

componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log('第二次 ', this.state.val);
}


render() {
return (
<div>
{this.state.val}
</div>
);
}
}

结果:

1
2
3
4
第一次 0
第二次 1

// 页面 2

立刻调用

如果要在同一个批量更新事务中立刻获得更新后的state,就要将相关操作放到setState的回调函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({ val: this.state.val + 1 }, () => {
console.log('第一次 ', this.state.val);
});
}

render() {
return (
<div>
{this.state.val}
</div>
);
}
}

结果:

1
2
3
第一次 1

// 页面 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
33
34
35
export default class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log('第一次 ', this.state.val);

this.setState({ val: this.state.val + 1 });
console.log('第二次 ', this.state.val);

setTimeout(() => {
console.log('第三次 ', this.state.val);

this.setState({ val: this.state.val + 1 });
console.log('第四次 ', this.state.val);

this.setState({ val: this.state.val + 1 });
console.log('第五次 ', this.state.val);
}, 0);
}


render() {
return (
<div>
{this.state.val}
</div>
);
}
}

执行的结果:

1
2
3
4
5
6
7
第一次  0
第二次 0
第三次 1
第四次 2
第五次 3

// 页面 3

为什么前两次的结果都是0呢?因为前面提过了,生命周期是批量更新事务,setState会在更新事务结束后统一执行,所以打印的都是0

这两次更新后state的结果是1,并非是2,因为这两次调用时this.state.val都是0,相当于执行this.setState({ val: 1 })两次,所以第三次打印的结果是1

setTimeout是异步任务,setState并非在批量更新事务中,setState会立即执行。所以后两次的console.log结果可以立刻获取到更新后的state

详细的源码解读放到以后慢慢看吧。

参考