谈到React,不得不提到的就是state,相信你一定用过很多次的setState,也知道setState是一个异步方法,正如官网说的:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

setState调用位置"#"

从React调用stateState的位置来看情况无非有2种:

  • 一种是在生命周期函数中 componentWillMountcomponentDidMountcomponentWillReceivePropsrender
  • 在各种handler中(包括组件本身元素的handler和通过this.props.handerXXX)

在handler中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
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler(){
this.setState({
count: this.state.count + 1
})
console.log("log 1 :", this.state.count); // log 1
this.setState({
count: this.state.count + 1
})
console.log("log 2 :", this.state.count); // log 2
}
render(){
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.clickHandler}>Count Add</button>
</div>
);
}
}

试着想一下当点击button的时候2次log的结果分别是什么?先不急着公布答案,在来看看下面的代码

在生命周期中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
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
};
}
componentWillMount(){
this.setState({
count: this.state.count + 1
})
console.log("log 3 :", this.state.count); // log 3
this.setState({
count: this.state.count + 1
})
console.log("log 4 :", this.state.count); // log 4
}
render(){
return (
<div>
<p>Count: {this.state.count}</p>
</div>
);
}
}

你一定知道在componentWillMount调用setState是会合并state而不会触发re-render,那么log3和log4分别输出多少呢?

setState调用方式"#"

众所周知,在JavaScript中充满了各种异步函数,当然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
28
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
};
}
componentWillMount(){
setTimeout(()=> {
this.setState({
count: this.state.count + 1
})
console.log("log 5 :", this.state.count); // log 5
this.setState({
count: this.state.count + 1
})
console.log("log 6 :", this.state.count); // log 6
}
}, 0)
}
render(){
return (
<div>
<p>Count: {this.state.count}</p>
</div>
);
}
}

上面的setState是被放入异步执行队列被执行的,那么log5和log6分别输出多少呢?(在各种handler回调中执行的异步setState类似这里的5和6)

答案"#"

log1和log2

log3和log4

log4和log6

现在看来setState似乎和在哪里调用没有关系,而是和调用方式有关系。那么为什么log1~4的结果显示的就像setState是一个异步函数呢?

从函数调用栈入手"#"

以下仅对比同步setState(log1、2)和异步setState(log5、6)的结果。

log1、2的call stack"#"

render函数中打上断点调试。

1
2
3
4
5
6
7
8
render(){
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.clickHandler}>Count Add</button>
</div>
);
}

查看调用栈:

不要被这么冗长的调用栈吓倒(微笑脸),注意观察图书箭头所指向的2个函数batcheUpdates,字面意思就是批量更新,我吗暂且先放一边,看看log 5、6的调用栈是怎样的:

log5、6的call stack"#"

老样子,打上断点:

1
2
3
4
5
6
7
8
render(){
debugger;
return (
<div>
<p>Count: {this.state.count}</p>
</div>
);
}

查看调用栈:

可以看到这里也有enqueueUpdate方法,那么这个enqueueUpdate究竟是何方神圣呢?
不急,要想知道答案的话就得去看看React的源码了。

1
2
3
4
5
6
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback);
}
};

其实我们所定义的组件都是继承自ReactComponent这个父类,通过this.updater.enqueueSetState(this, partialState)的方式去设置state。那么this.updater又是何方神圣呢?
通过查看ReactCompositeComponent.js源码发现了以下代码:

1
2
3
4
//ReactCompositeComponent.mount
var updateQueue = transaction.getUpdateQueue();// 得到更新队列
// 略
return new Component(publicProps, publicContext, updateQueue);

上面的Component就是我们自定义组件的构造类函数,在实例化自定义组件的时候传入。我们再来看看React自定义组件的够着函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
//React.createClass
var Constructor = identity(function(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue; // <== 这里赋值给了组件实例
this.state = null;
var initialState = this.getInitialState ? this.getInitialState() : null;
this.state = initialState;
});

一切真想大白了,现在我们知道了this.updater其实就是transaction.getUpdateQueue()返回的一个对象,我们接着查找相关源码,发现了this.updater.enqueueSetState的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
//略
if (!internalInstance) {
return;
}
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState); // 这里将设置的state push到internalInstance._pendingStateQueue上
enqueueUpdate(internalInstance); // 调用队列更新
}

上面的代码你一定有很多疑惑,不要着急,这里的internalInstance其实就是ReactCompositeComponent的一个实例,它负责管理我们的自定义组件的渲染、更新和销毁。在此我们仅需要看最后一层调用就可以了,还记得我们上面函数调用栈的2个enqueueUpdate吗,这个就是第一个enqueueUpdate,他就是一个简单的函数:

1
2
3
function enqueueUpdate(internalInstance) { // ReactCompositeComponent 的实例
ReactUpdates.enqueueUpdate(internalInstance);
}

这个函数调用的是ReactUpdates.enqueueUpdate方法,这就是第二个enqueueUpdate。你可能已经有点晕了,但是没关系坚持去理解,多尝试几次就好了(微笑脸)
通过查看enqueueUpdate的源码发现就是一个对象,负责的是React有关的更新、设置回调等。我们看看ReactUpdates.enqueueUpdate代码:

1
2
3
4
5
6
7
8
9
10
function enqueueUpdate(component) {
ensureInjected(); // ==> 这个就是React 建成 transaction是否成功注入的一个函数,没有实际逻辑功能只是用来在console中提示,暂时不用管
if (!batchingStrategy.isBatchingUpdates) { // batchingStrategy.isBatchingUpdates就是一个布尔值
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);// 看名字就知道 脏组件列表
}

注意:上面的代码是React调用setState后是否是异步执行的真正原因!!!
这里的逻辑其实很难将清楚,但是你可以简单理解为就是判断batchingStrategy.isBatchingUpdates是否为false,为false就立即调用batchingStrategy.batchedUpdates执行更新,否则就将当前的脏组件放入dirtyComponents中,等待batchingStrategy.isBatchingUpdates置为false之后批量更新。其实batchingStrategy就是一个React策略类对象,它很简单,简单到只有2个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false, // 布尔值标识
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 标识置为true
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
}
};

ReactDefaultBatchingStrategy是React的一个批量更新策略对象,只要当前在React的批量更新当作,ReactDefaultBatchingStrategy.isBatchingUpdates就为true,此时同步调用setState由于isBatchingUpdatestrue,只能放到dirtyComponents当中,等待当前批量更新完毕后从dirtyComponents中一个个拿出dirtyComponent执行更新。而当异步方式调用setState的时候,此时React批量更新完毕,isBatchingUpdatesfalse,通过执行batchingStrategy.batchedUpdates立即更新当前state。讲到这里已经可以解释上述几处log的区别了:

  • log1、2和log3、4是一类的,因为他们是同步调用setState,此时React正处于批量更新当作,所以2次setState只为讲当前component push到dirtyComponents中,所以console到的count还是0,并且最后2次state做了pending处理后count变为1;
  • log5、6由于在setTimeout中调用,此时React已经批量更新完毕,isBatchingUpdatesfalse,直接执行更新,state立即加1,这也就解释了5、6处setState就好像是同步更新state一样。
    注意:上面的transaction.perform是关于React的事务,在React中调用了大量的事务来保证批量渲染、更新和卸载的正确执行。这里关于transaction后续我会单独讲解,感兴趣的童鞋也可以自行查看相关源码。

    总结"#"

  • React中自定义组件统一都继承自ReactComponent,在ReactComponent上定义了setState方法。

  • React通过updater来执行更新,这个updater是在生成组件实例中传入到构造函数当中的。
  • updater其实就是ReactUpdates,定义的enqueueSetState方法通过层层调用,执行更新。
  • React通过一个ReactDefaultBatchingStrategy对象管理更新,这个对象有一个 isBatchingUpdates属性,标识当前是否正在更新,如果当前正在更新则将待更新的组件pushdirtyComponents,等待React批量更新完成后统一更新,否则执行batchingStrategy.batchedUpdates立即更新当前state

最后"#"

关于React是如何进入批量更新,和如何退出批量更新的问题稍后我们讲到,需要结合到transaction统一来讲。最后,精力有限,难民有疏漏和错误,欢迎各位指正。(^=^)

附上demo地址==> >>>我是地址