揭秘 setState 机制

前言:state 是 react 中重要的概念, react 是通过管理状态来实现对组件的管理,那么 react 是如何控制组件的状态 又是如何利用状态来管理组件的呢?

我们所知道的版本 大概是 通过 this.state 来访问 state,通过 setState() 方法来更新 state,当 this.state() 被调用的时候 React 会重新调用 render 方法来重新渲染 UI

那好的 先来看一道题吧

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

componentWillMount(){
this.setState({val:this.state.val+1});
console.log('componentWillMount第一次输出',this.state.val)
this.setState({val:this.state.val+1});
console.log('componentWillMount第二次输出',this.state.val)
}
componentDidMount(){
// debugger;
this.setState({val:this.state.val+1});
console.log('componentDidMount第一次输出',this.state.val)
this.setState({val:this.state.val+1});
console.log('componentDidMount第二次输出',this.state.val)
setTimeout(()=>{
// debugger;
console.log('开始setTimeout',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 null;
}

}

这道题的答案是 0 0 1 1 2 3 4

假如结果与你心中的答案并不完全相同,那么你应该感兴趣这背后究竟发生了什么.

了解 setState

  1. setState 是同步执行的 但是 state 并不一定会同步更新(异步更新)
1
实际上react的异步更新通过一个队列机制来实现,当执行state时 需要将更新的state合并后放入状态队列而不会立刻更新 队列机制可以高效的批量更新state 如果在非构造方法里更改值  类似 this.state.name='yishu' 是不会被放到状态队列中 当下次调用setState并对状态队列进行合并时 将会忽略它而造成无法预知的错误
  1. setState 在 React 生命周期和合成事件中批量覆盖执行
1
在React的生命周期钩子和合成事件中,多次执行setState,会批量执行,多次同步执行的setState,会进行合并,类似于Object.assign
  1. setState 在原生事件,setTimeout,setInterval,Promise 等异步操作中,state 会同步更新
1
当执行到 setTimeout 的时候,把它丢到列队里,并没有去执行,而是先执行的 finally 主进程代码块,等 finally 执行完了, isBatchingUpdates 又变为了 false ,导致最后去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支,所以表现就会和原生事件一样,可以同步拿到最新的state的值。

关于 setState 这个方法 源码记载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ReactComponent.prototype.setState = function(partialState, callback) {
//...
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};

enqueueSetState: function(publicInstance, partialState) {
//...
var queue =internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);

enqueueUpdate(internalInstance);
},

setState 方法实际上会执行 enqueueSetState 方法 通过_pendingStateQueue 更新队列进行合并操作 最终通过 enqueueUpdate 执行 state 更新

setState 调用栈

如图:通过变量 isBatchingUpdate 来决定当前是应该走批量更新 还是立即更新 为 true 时 说明当前在批量更新模式 为 false 的话 会立即更新

为了更好的理解 涉及到部分源码

enqueueUpdate 代码如下:

1
2
3
4
5
6
7
8
9
10
11
function enqueueUpdate(component) {

// 如果不处于批量更新模式 更新
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果处于批量更新模式 则将该组件保存在 dirtyComponents 中
dirtyComponents.push(component);
}


那么这个 batchingStrategy 究竟是做什么的? 其实它只是一个简单的对象,定义了 isBatchingUpdates 和 batchedUpdates 方法 其中 transaction.perform 的调用 涉及到了事务的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// batchedUpdates 方法
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,

batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
},
};

事务机制

事务就是将需要执行的方法使用 wrapper 封装起来 再通过事务提供的 perform 方法执行

执行 perform 之前 先执行 wrapper 中的 init 方法 执行完 perform 之后 再执行 所有的 close 方法
假如有一个事务 test 执行顺序表现为

init->test->close

揭秘 setState 机制

那么 说了这么多,事务是怎么导致前面所述的 setState 的各种不同表现呢.

在整个 React 组件渲染到 Dom 中的过程就处于一个大的事务中 ,在生命周期和合成事件执行前后都会执行 init 和 close,init 会调用 batchedUpdate 方法将 isBatchingUpdates 变量置为 true,开启批量更新,而 close 会将 isBatchingUpdates 置为 false,setState 的更新会被存入队列中,待同步代码执行完后,再执行队列中的 state 更新。

而在原生事件和异步操作中,不会执行 pre 钩子,或者生命周期的中的异步操作之前执行了 pre 钩子,但是 pos 钩子也在异步操作之前执行完了,isBatchingUpdates 必定为 false,也就不会进行批量更新

获取正确的 state 值

以下:

  1. setState 函数式
  2. 放到 setTimeout,Promise 等异步中执行
  3. 放到 componentDidUpdate 中

说在最后的话

所以 开篇的结果应该可以理解了吧

我们把 didMount 中四次调用归类,前两次一类 因为它们在同一个调用栈中执行 setTimeout 中的两次属于另一类,我们重点看第一类,早在 setState 调用之前 ReactDefaultBatchingStrategy.isBatchingUpdates 已经被设置为 true,所以两次的 setSate 并没有生效 而是被放进了队列中
再看 setTimeout 中的两次 state 此时的 isBatchingUpdates 为 false,这也就导致了心的 state 马上生效 没有走到队列的分支(可参考调用栈图)也就是说 第一次执行 setState 时 值就为 1 加 1 之后变为 2 第二次打印同理

参考:深入 react 技术栈一书 希望通过 setState 深入源码 知其然也知其所以然