React 15 的 setState(下)

state真正更新,了解一下 ->

上次,我们谈到了setState的批量更新策略,还记得上次说的transcation.perform的执行流程以及批量更新的transaction的两个wrapper FLUSH_BATCHED_UPDATESRESET_BATCHED_UPDATES吗?

再次放上transaction的这张图

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

经过上篇的分析,我们了解到setState的执行会将component放入dirtyComponents中,perform执行完anyMethod后会依次执行wrapperclose方法。真正的state更新会在perfrom调用的FLUSH_BATCHED_UPDATES.close里去实现。那FLUSH_BATCHED_UPDATES.close又是怎么实现更新的?带着这个疑问,我们继续读下去

还是依照调用栈的顺序,挨个看函数

调用栈

close

FLUSH_BATCHED_UPDATESclose函数

1
2
3
4
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

flushBatchedUpdates处打上断点

发现调用栈如下

1
2
3
4
5
flushBatchedUpdates
closeAll
perform
batchedUpdates
// ...

flushBatchedUpdates

这个方法在ReactUpdates.js 文件中。这个文件也有一个自己的transaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// transaction
_assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
},
destructor: function () {
this.dirtyComponentsLength = null;
CallbackQueue.release(this.callbackQueue);
this.callbackQueue = null;
ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
this.reconcileTransaction = null;
},
perform: function (method, scope, a) {
// Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
// with this transaction's wrappers around it.
return Transaction.perform.call(this, this.reconcileTransaction.perform, this.reconcileTransaction, method, scope, a);
}
});

注意到这个transaction重写了perform方法,它的perform在执行的时候,会调用reconcileTransaction.perform方法。也就是真正的方法被包了2个transaction的wrappers:

// perform 过程
ReactUpdatesFlushTransaction.wrappers的initialize
|
V
reconcileTransaction.wrappers的initialize
|
V
anyMethod
|
V
reconcileTransaction.wrappers的close
|
V
ReactUpdatesFlushTransaction.wrappers的close

看一下ReactUpdatesFlushTransaction的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
// transcation的wrappers
var NESTED_UPDATES = {
initialize: function () {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function () {
if (this.dirtyComponentsLength !== dirtyComponents.length) {
// Additional updates were enqueued by componentDidUpdate handlers or
// similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
// these new updates so that if A's componentDidUpdate calls setState on
// B, B will update before the callback A's updater provided when calling
// setState.
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0;
}
}
};
var UPDATE_QUEUEING = {
initialize: function () {
this.callbackQueue.reset();
},
close: function () {
this.callbackQueue.notifyAll();
}
};

再看一下flushBatchedUpdates的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var flushBatchedUpdates = function () {
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
// updates enqueued by setState callbacks and asap calls.
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};

来理一遍transaction的perform的执行顺序

  1. NESTED_UPDATES.initialize
    1. 记录dirtyComponents的长度
  2. UPDATE_QUEUEING.initialize
    1. 重置回调队列
  3. 执行reconcileTransaction.perform 包裹的 runBatchedUpdates
  4. NESTED_UPDATES.close
    1. 如果有新的dirtyComponents产生,调用flushBatchedUpdates;如果没有,重置dirtyComponents
  5. UPDATE_QUEUEING.close
    1. 执行回调队列里的所有函数

看到,这里的第5步,有没有想到什么?上一篇说到的setState的第二个函数的执行,就是在这个阶段执行的。也就是等transaction的最后,才会执行这里的回调,而不是变更一个就执行一个。

runBatchedUpdates

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
41
42
43
44
45
46
47
48
49
50
51
52
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
!(len === dirtyComponents.length) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Expected flush transaction\'s stored dirty-components length (%s) to match dirty-components array length (%s).', len, dirtyComponents.length) : _prodInvariant('124', len, dirtyComponents.length) : void 0;
// Since reconciling a component higher in the owner hierarchy usually (not
// always -- see shouldComponentUpdate()) will reconcile children, reconcile
// them before their children by sorting the array.
dirtyComponents.sort(mountOrderComparator);
// Any updates enqueued while reconciling must be performed after this entire
// batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
// C, B could update twice in a single batch if C's render enqueues an update
// to B (since B would have already updated, we should skip it, and the only
// way we can know to do so is by checking the batch counter).
updateBatchNumber++;
for (var i = 0; i < len; i++) {
// If a component is unmounted before pending changes apply, it will still
// be here, but we assume that it has cleared its _pendingCallbacks and
// that performUpdateIfNecessary is a noop.
var component = dirtyComponents[i];
// If performUpdateIfNecessary happens to enqueue any new updates, we
// shouldn't execute the callbacks until the next render happens, so
// stash the callbacks first
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var namedComponent = component;
// Duck type TopLevelWrapper. This is probably always true.
if (component._currentElement.type.isReactTopLevelWrapper) {
namedComponent = component._renderedComponent;
}
markerName = 'React update: ' + namedComponent.getName();
console.time(markerName);
}
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);
if (markerName) {
console.timeEnd(markerName);
}
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
}
}
}
}

看到runBatchedUpdates做的一些事

  1. 对dirtyComponents做排序,让父组件在前面
  2. updateBatchNumber++ 更新批量更新的次数
  3. 遍历dirtyComponents
    1. 进行更新
    2. 有回调,就加入到回调队列

看到这,或许你跟我有一样的疑问

  1. 为什么要先处理父组件?
  2. 为什么要记录批量更新的次数?

别着急,这些我们先放一放,先去看看更新里做了什么

performUpdateIfNecessary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ReactReconciler.performUpdateIfNecessary
performUpdateIfNecessary: function (internalInstance, transaction, updateBatchNumber) {
if (internalInstance._updateBatchNumber !== updateBatchNumber) {
// The component's enqueued batch number should always be the current
// batch or the following one.
process.env.NODE_ENV !== 'production' ? warning(internalInstance._updateBatchNumber == null || internalInstance._updateBatchNumber === updateBatchNumber + 1, 'performUpdateIfNecessary: Unexpected batch number (current %s, ' + 'pending %s)', updateBatchNumber, internalInstance._updateBatchNumber) : void 0;
return;
}
if (process.env.NODE_ENV !== 'production') {
if (internalInstance._debugID !== 0) {
ReactInstrumentation.debugTool.onBeforeUpdateComponent(internalInstance._debugID, internalInstance._currentElement);
}
}
internalInstance.performUpdateIfNecessary(transaction);
if (process.env.NODE_ENV !== 'production') {
if (internalInstance._debugID !== 0) {
ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID);
}
}
}

会检查component的批次是否和当前一致。

这里的performUpdateIfNecessary, 又调用了component.performUpdateIfNecessary

ReactCompositeComponent 的 performUpdateIfNecessary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
* is set, update the component.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
},

之前的要更新的state都存在_pendingStateQueue里,这里会检测是否有待更新的状态,如果有,则执行updateComponent

updateComponent

终于走到更新组件这一步了

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
var inst = this._instance;
!(inst != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Attempted to update component `%s` that has already been unmounted (or failed to mount).', this.getName() || 'ReactCompositeComponent') : _prodInvariant('136', this.getName() || 'ReactCompositeComponent') : void 0;
var willReceive = false;
var nextContext;
// Determine if the context has changed or not
if (this._context === nextUnmaskedContext) {
nextContext = inst.context;
} else {
nextContext = this._processContext(nextUnmaskedContext);
willReceive = true;
}
var prevProps = prevParentElement.props;
var nextProps = nextParentElement.props;
// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
willReceive = true;
}
// An update here will schedule an update but immediately set
// _pendingStateQueue which will ensure that any state updates gets
// immediately reconciled instead of waiting for the next batch.
if (willReceive && inst.componentWillReceiveProps) {
if (process.env.NODE_ENV !== 'production') {
measureLifeCyclePerf(function () {
return inst.componentWillReceiveProps(nextProps, nextContext);
}, this._debugID, 'componentWillReceiveProps');
} else {
inst.componentWillReceiveProps(nextProps, nextContext);
}
}
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) {
if (process.env.NODE_ENV !== 'production') {
shouldUpdate = measureLifeCyclePerf(function () {
return inst.shouldComponentUpdate(nextProps, nextState, nextContext);
}, this._debugID, 'shouldComponentUpdate');
} else {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
}
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
}
}
}
if (process.env.NODE_ENV !== 'production') {
process.env.NODE_ENV !== 'production' ? warning(shouldUpdate !== undefined, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : void 0;
}
this._updateBatchNumber = null;
if (shouldUpdate) {
this._pendingForceUpdate = false;
// Will set `this.props`, `this.state` and `this.context`.
this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
} else {
// If it's determined that a component should not update, we still want
// to set props and state but we shortcut the rest of the update.
this._currentElement = nextParentElement;
this._context = nextUnmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
}
},

代码很长,但是不用太在意。

关注一下下面几点:

  • 这个方法中间调用了一些更新阶段生命周期方法componentWillReceivePropsshouldComponetUpdate

  • 把component的_updateBatchNumber重置为null

  • 计算下个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
    // updateComponent 调用
    var nextState = this._processPendingState(nextProps, nextContext);
    // _processPendingState
    _processPendingState: function (props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;
    if (!queue) {
    return inst.state;
    }
    if (replace && queue.length === 1) {
    return queue[0];
    }
    var nextState = _assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
    var partial = queue[i];
    _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }
    return nextState;
    },

这里会依次遍历_pendingStateQueue中的元素,将它们合并到nextState里。如果队列是函数,则传入上个nextState,将函数执行的结果作为一个state进行合并。到这,你有没有想起什么?上一篇,我们提到的获取新的state的方式的第一种,就是setState第一个参数为函数的方式,它能拿新的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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // updateComponent 调用
    this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
    // _performComponentUpdate
    _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
    var _this2 = this;
    var inst = this._instance;
    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
    prevProps = inst.props;
    prevState = inst.state;
    prevContext = inst.context;
    }
    if (inst.componentWillUpdate) {
    if (process.env.NODE_ENV !== 'production') {
    measureLifeCyclePerf(function () {
    return inst.componentWillUpdate(nextProps, nextState, nextContext);
    }, this._debugID, 'componentWillUpdate');
    } else {
    inst.componentWillUpdate(nextProps, nextState, nextContext);
    }
    }
    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;
    this._updateRenderedComponent(transaction, unmaskedContext);
    if (hasComponentDidUpdate) {
    if (process.env.NODE_ENV !== 'production') {
    transaction.getReactMountReady().enqueue(function () {
    measureLifeCyclePerf(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), _this2._debugID, 'componentDidUpdate');
    });
    } else {
    transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst);
    }
    }
    },

这里做的事情

  1. 执行componentWillUpdate
  2. 更新props,state
  3. 进行渲染_updateRenderedComponent
  4. 执行componentDidUpdate

对于怎么渲染更新的组件,这里不往下看了

到这我们总算找到了flushBatchedUpdate主要做的事updateComponent

总结

做了这么多事,其实就得到了下面一条信息:

ReactDefaultBatchingStrategy中的transaction会在close阶段,处理dirtyComponnets,更新它们的props,state及更新视图。

到这,state更新的过程也就结束了。

我们在回过头,想想之前遇到的问题

Q & A

同一批次相同组件的处理

Q: 会重复更新吗?

A:不会,每个component在加入dirtyComponents时,会标记component的_updateBatchNumber为当前批次,批量更新时,会检查这个批次是否是当前需要处理的批次,不是就不会更新了。假设dirtyComponents有两个相同的component,A1和A2,遍历执行更新,发现A1的批次和当前一致,会更新,然后把A1组件_updateBatchNumber置为null。当遍历到A2时,因为批次(被置为null了)不匹配,就不会更新。

为什么要对dirtyComponents排序

因为父组件的更新通常会造成子组件的更新,为了避免重复的更新工作,对dirtyComponents排序,先更新父组件,这样父组件更新的过程中,可以一道把子组件更新了。

执行过程中是否可能有新的state产生,如何处理

有可能的,比如父组件变更导致子组件的一些setState(比如在willReceiveComponent里),它会加入到dirtyComponents中,但是批次不同,等这批次更新完后(transaction没有结束),再次进行flushBatchedUpdates()进行更新

批次计数有何作用

避免重复的更新,做一个标志,表示更没更新