React 15 的 setState(上)

setState 批量更新策略,了解一下 ->

之前面试被问到setState是同步的还是异步的,答得很糟糕,现在有时间了,结合网上查找的资料,跑一跑demo,来看看这个问题。

ps: 这里的同步异步是说执行setState后,获取的state是否就是更新后的state

看下官网上的内容

React may batch multiple setState() calls into a single update for performance.

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

React可能对setState进行批量处理, 你拿到state不一定是更新后的state

来看下下面这个例子(React 15.6.2)

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
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
}
}
componentDidMount() {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count); // 第 1 次 log
this.setState({
count: this.state.count + 1
})
console.log(this.state.count); // 第 2 次 log
setTimeout(() => {
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 第 3 次 log
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 第 4 次 log
}, 0);
}
render() {
return (
<div>
{this.state.count}
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)

猜猜上面的输出是什么?

答案是:0 0 2 3

到chrome里运行看看,在setState处打上断点,查看一下调用栈

调用栈

setState

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

enqueueSetState

1
2
3
4
5
6
7
8
9
10
11
12
13
enqueueSetState: function (publicInstance, partialState) {
// ...
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},

做的主要事情:

  1. partialState存放_pendingStateQueue(待更新队列)里
  2. 执行enqueueUpdate

enqueueUpdate

enqueueUpdate里直接调用ReactUpdates.enqueueUpdate

ReactUpdates.enqueueUpdate

先来看下执行到的enqueueUpdate里干了什么吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enqueueUpdate(component) {
ensureInjected();
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

这里batchingStrategy.isBatchingUpdates表示是否处于批量更新过程。

如果处于批量更新过程,将需要更新的组件添加到dirtyComponents里面

如果不是,则执行batchedUpdates操作。

/*
*       this.setState()
*             |
*     存入 _pendingStateQueue
*             |
*   +----是否处于批量更新中----+
*   | Y                     | N
*   |                       |
*   component 保存        batchedUpdates
*   dirtyComponents中     
*/

batchedUpdates

上一步的判断条件和调用方法都是在batchingStrategy上的,看一下ReactDefaultBatchingStrategy.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
/**
* Call the provided function in a context within which calls to `setState`
* and friends are batched such that components aren't updated unnecessarily.
*/
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};

看到这个isBatchingUpdates的初始值是false

batchedUpdates执行,会将isBatchingUpdates属性设为true

isBatchingUpdates之前为false, 会执行transaction.perform来执行更新

transaction是什么呢?我们先来了解一下

transaction

transaction.js的代码中,已经很形象地画出了perform执行的过程

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+

transcation在创建时会注入wrappers,每个wrapperinitializeclose方法,当执行被perform包装的方法时,会依次执行每个wrapperinitialize方法,然后执行真正的方法,最后再依次执行wrapperclose方法。它提供了getTransactionWrappers这个抽象方法,可以去设置wrappers

看一下使用方式:

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
var _assign = require('object-assign');
var Transaction = require('./Transaction');
var emptyFunction = require('fbjs/lib/emptyFunction');
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
}
});
var transaction = new ReactDefaultBatchingStrategyTransaction();

看到 ReactDefaultBatchingStrategyTransaction 设置两个wrapper,先是FLUSH_BATCHED_UPDATES,然后是RESET_BATCHED_UPDATES

两个wrapper的initialize都没做事

FLUSH_BATCHED_UPDATESclose调用了flushBatchedUpdates做批量收集后的刷新工作,这里我把它想成state的真正更新。

RESET_BATCHED_UPDATESclose里重置isBatchingUpdatesfalse,表示当前批量处理流结束

demo中两种setState的结果

来看前2个setState调用栈

1
2
3
4
5
6
7
8
ReactComponent.setState
componentDidMount
//...
perform
batchedUpdates // from ReactDefaultBatchingStrategy.js
ReactMount._renderNewRootComponent
// ...
index.js

发现:

  1. setState 处在一个 transaction中

可以看到setState的执行栈里出现了batchedUpdates,而根据上面的代码我们知道,它会把isBatchingUpdates设成true,另外perform也在栈中,说明这个transaction没有结束。

因此setState执行到enqueueUpdate时,只是将对应的component暂存到dirtyComponents中,然后setState就执行完了。接着执行componentDidMount中的下一条语句console.log(this.state.count),而不是进入到这个transaction的close阶段的流程,所以这时state的值未发生变化。打印出的state还是原来的值。

setState步骤:

  1. 执行setState
  2. 经过层层调用(没有perform),将component加入dirtyComponents
  3. setState结束

再去看看后面2个的setState,因为放在了setTimeout里,而setTimeout 是异步的,它等之前的同步代码执行完后,才会调用。而同步代码执行完后isBatchingUpdates已经恢复成false了(transaction.perform在close时,把ReactDefaultBatchingStrategy.isBatchingUpdates被设成false)。

因此后2个setState进入batchedUpdates会调用一次transaction.perform处理(新建一个事务),包装的函数为enqueueUpdate,会把这次component放入dirtyComponents,perform执行完enqueueUpdate后,会去更新state,把ReactDefaultBatchingStrategy.isBatchingUpdates被设成false。perform调用结束后,setState也执行完了,接着往下执行console.log(this.state.count),这时打印出的结果就为更新的结果。

setState步骤:

  1. 执行setState
  2. 经过层层调用,执行到transaction.perform
    1. enqueueUpdate
      1. component加入dirtyComponents
    2. 更新state
  3. setState结束

打印出的结果

0 // {count: 1} 加入 dirtyComponents
0 // {count: 1} 加入 dirtyComponents
dirtyComponent被处理 state 变为 1
2 // {count: 2} 加入dirtyComponents,处理
3 // {count: 3} 加入dirtyComponents,处理

如何获取React更新后的state

看到以上,我们知道React对state的处理可能是“同步”,也可能是“异步”的

那有哪些方法是能获取到更新后的state?

  • 官网推荐的方式setState(fn),传入的state会是上次的结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    this.setState((state) => {
    console.log(state.count) // 0
    return {
    count: state.count + 1
    }
    })
    this.setState((state) => {
    console.log(state.count) // 1, 上个setState的state结果
    return {
    count: state.count + 1
    }
    })
  • setState的回调,注意是批量更新后的结果,而不是一次setState后的结果,即它的回调是state批量更新后的才会调用的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    this.setState({
    count: this.state.count + 1
    }, () => {
    console.log(this.state.count) // 2
    })
    this.setState({
    count: this.state.count + 2
    }, () => {
    console.log(this.state.count) // 2
    })
  • 上面我们看到的setTimeout的方式

什么时候会出现批量更新?

既然我们知道,batchedUpdates会开启一个transaction,进行批量更新,那只要在代码里搜索ReactUpdates.batchedUpdates,那找到的地方就可能是我们说的会批量更新的情况

部分搜索结果:

  • ReactMount.js

挂载和卸载阶段的 _renderNewRootComponentunmountComponentAtNode

这里涉及的生命周期有:componentWillMountcomponentDidMount

  • ReactEventListener.js

dispathEvent,也就是React的事件,会走异步更新

如何批量更新

当你需要进行多次setState操作的时候,但是不在react的transaction时,你可以考虑用batchedUpdates来进行批量更新操作。React把batchedUpdates暴露了出来unstable_batchedUpdates

1
2
3
4
5
6
7
8
9
10
11
var ReactDOM = {
findDOMNode: findDOMNode,
render: ReactMount.render,
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
version: ReactVersion,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
/* eslint-enable camelcase */
};

好了,到这你是否又对setState又有了进一步的了解呢?

还是和我一样心存疑问:

  • state真正的更新又是如何处理的?

带着这些问题, 可以看下篇。

参考资料

React 源码剖析系列 - 解密 setState