this.setState()是React组件开发中非常常用的方法,通常用于更新state,继而更新组件。那么setState是如何工作的呢,下面一起探讨一下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
29
30
31
32
33
34
35
36
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}

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

this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log

setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log

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

render() {
return <p>Example</p>;
}
};

ReactDOM.render(
<Example />,
document.getElementById('root')
);

打开console控制台,发现输出的结果是

1
0,0,2,3

同样都是执行两次,为什么值的变化不一致呢?下面我们从源码上来分析看看,setState是如何工作的。

setState处理机制

react/lib/ReactComponent.jsReactComponent.js
1
2
3
4
5
6
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
}
react-dom/lib/ReactUpdateQueue.jsReactUpdateQueue.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var ReactUpdateQueue = {
enqueueSetState: function (publicInstance, partialState) {

// 获取React Component的内部实例,在comp._reactInternalInstance属性上存储着
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

if (!internalInstance) {
return;
}

// 将state插入到内部component实例的state队列中
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);

// 处理更新
enqueueUpdate(internalInstance);
}
}

function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
ReactUpdatesReactUpdates.js
1
2
3
4
5
6
7
8
function enqueueUpdate(component) {
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}

dirtyComponents.push(component);
}

抽象出关键流程可以看出,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
            ---------------------------
| this.setState(newState) |
---------------------------
|
----------------------------------
| newState存入_pendingStateQueue |
----------------------------------
|
------是否处于batch update中-----
| |
| Y | N
| |
-------------------- ----------------------
| component保存在 | | 遍历dirtyComponents|
| dirtyComponents中| | 调用updateComponent|
| | | 更新state |
-------------------- ----------------------

在执行setState的时候,React Component将newState存入了自己的等待队列,然后使用全局的批量策略对象batchingStrategy来查看当前执行流是否处在批量更新中,如果已经处于更新流中,就将记录了newState的React Component存入dirtyeComponent中,如果没有处于更新中,遍历dirty中的component,调用updateComponent,进行state或props的更新,刷新component。

在这里大概可以猜到,是这里的判断和分支导致了setState的不同行为。是batchingStrategy提供了isBatchingUpdates判断条件和batchedUpdates处理方法。batchingStrategy默认使用React提供的ReactDefaultBatchingStrategy,我们可以看看在它的batchedUpdates中做了哪些事情。

ReactDefaultBatchingStrategyReactDefaultBatchingStrategy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,

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

ReactDefaultBatchingStrategy.isBatchingUpdates = true;

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

在batchedUpdates可以看到,有一个transaction.perform来执行更新处理,由此引出了transaction(事务流)概念。React里的很多更新处理都采用了transaction机制,下面介绍一下transaction的工作原理。

transaction事务流

源码中提供了transaction的transaction的ascall流程图,对transaction的执行流解释的非常形象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>

transaction实际上做的事情就是将要执行的method使用wrapper封装起来,用提供的perform方法来调用method。在调用的过程中,会先顺序调用wrapper中注册的initialize方法,然后执行method方法,最后顺序调用wrapper中注册的close方法。initialize和close可以是调用transaction的模块自定义的。

transaction提供了所需的mixin方法,当模块需要使用transaction时,可以将它提供的方法混入到自己的模块中。其中,transaction提供了getTransactionWrappers方法,便于模块注册wrapper,定义method所需的前置函数(initialize)和后置函数(close)。实现上,是返回一个数组,每个数组元素是一个对象,这个对象就是wrapper了,其中可以定义initialize()和close()。

下面列举一个模块应用transaction的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Transaction = require('./Transaction');

var MyTransaction = function() {
// 处理自己的事务
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('before method perform');
},
close: function() {
console.log('after method perform');
}
}];
};
});

var transaction = new MyTransaction();
var testMethod = function() {
console.log('test');
}
transaction.perform(testMethod);

console输出

1
2
3
before method perform
test
after method perform

调用学会了,那么其中的initialize和close方法可以做些什么事情呢。从ReactDefaultBatchingStrategy中可以看出,在处理队列更新时,执行流正处于一个batchingUpdate中,可以在initialize做一些准备工作,在close中做收尾工作,比如在close中将isBatchingStrategy设为false,表示当前批量处理流结束,做批量收集后的刷新工作,刷新组件渲染。

setState调用栈分析

了解了transaction之后,回到最初的话题。transaction机制和开头的setState的不同表现有什么关系呢?通过调试setState,观察调用栈可以发现,前2个setState调用栈相似,后2个setState相似,可以归为2类分析。
第1类setState调用栈如下。

第2类setState调用栈如下。

两类调用栈相比,显然第1个调用栈更加复杂。在第1个调用栈中,发现调用setState前出现了close,perform,batchedUpdates等调用。原来在调用setState,执行流已经处于一个transaction中了。

在往前分析,可以发现是_renderNewRootComponent方法调用了batchedUpdates,原来整个React Component的渲染过程就处在一个transaction中。

这样就可以理解为什么第1次和第2次的setState执行后值没变了。因为在ComponentDidMount中调用setState时,渲染周期还没有结束,batchingStrategy中的isBatchingUpdates还是true,setState对应的components被存入dirtyComponents暂存起来,所以前2次setState之后的this.state.val的结果都是 0。

再观察第2类调用栈,使用了setTimeout,会在结束当前调用栈之后执行,这时渲染周期已经结束,batchingStrategy中的isBatchingUpdates为false。setState会在执行流中调用,不会进入dirtyComponents存储,所以新的state会立马生效,打印第3次和第4次的this.state.val,就会出现生效后的值了。

transaction使用场景

源码中列举了适用transaction的场景。

  • 在一次 DOM reconciliation(调和,即 state 改变导致 Virtual DOM 改变,计算真实 DOM 该如何改变的过程)的前后,保证 input 中选中的文字范围(range)不发生变化
  • 当 DOM 节点发生重新排列时禁用事件,以确保不会触发多余的 blur/focus 事件。同时可以确保 DOM 重拍完成后事件系统恢复启用状态。
  • 当 worker thread 的 DOM reconciliation 计算完成后,由 main thread 来更新整个 UI
  • 在渲染完新的内容后调用所有 componentDidUpdate 的回调

参考文章

React 源码剖析系列 - 解密 setState
深入理解 React 的 batchUpdate 机制