在React中,组件的更新本质上都是由setState操作改变state引起的。因此组件更新的入口在于setState,同样经过撸源码和打断点分析画了以下的组件更新的流程图:
setState的定义在组件mountComponent的时候定义:
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
复制代码
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
复制代码
所以setState的真正的定义在 ReactUpdateQueue.js
enqueueSetState: function (publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
// 将state添加到对应的component的_pendingStateQueue数组中。
queue.push(partialState);
enqueueUpdate(internalInstance);
}
enqueueCallback: function (publicInstance, callback, callerName) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
if (!internalInstance) {
return null;
}
// 将callback添加到对应的component的_pendingCallbacks数组中。
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
enqueueUpdate(internalInstance);
}
复制代码
两个方法最后都调用enqueueUpdate:
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
// 确认需要的事务是否注入了。
ensureInjected();
// batchingStrategy.isBatchingUpdates为false的时候,
// 或者说当不处于批量更新的时候,用事务的方式批量的进行component的更新。
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 当处于批量更新阶段时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中
dirtyComponents.push(component);
}
复制代码
这里需要注意enqueueUpdate中根据batchingStrategy.isBatchingUpdates分别进入不同的流程,当isBatchingUpdates为true的时候表示已经处于批量更新的过程中了,这时候会将所有的有改动的组件push到dirtyComponents中。当isBatchingUpdates为false的时候会执行更新操作,这里先认为当isBatchingUpdates为false的时候进行的操作是更新组件,实际上的过程是更复杂的,稍后马上解释具体的过程。这里我们先理解下React中的事务的概念,事务的概念根据源码中的注释就可以非常清楚的了解了:
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
复制代码
简单来说当使用transaction.perform执行方法method的时候会按顺序先执行WRAPPER里面的initialize方法然后执行method最后再执行close方法。
React提供了基础的事务对象Transaction,不同的事务的区别就在于initialize和close方法的不同,这个可以通过定义getTransactionWrappers方法来传入WRAPPER数组,具体的用法看下源码就好了,不过实际使用中是不会要自己去定义事务的,当然要的话也阻止不了~。
回到enqueueUpdate,其调用的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定义了:
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);
}
}
};
复制代码
可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程。
对于enqueueUpdate的效果就是,当执行enqueueUpdate的时候如果isBatchingUpdates为true的话(已经处于批量执行操作),则不会进行更新操作,而是将改动的component添加到dirtyComponents数组中;如果isBatchingUpdates为false的话,会执行batchedUpdates将isBatchingUpdates置为true然后调用enqueueUpdate方法,这个时候会用事务的方式来执行enqueueUpdate。
根据流程图可以知道,事务ReactDefaultBatchingStrategyTransaction的initialize是foo没有任务操作,接着会执行method即:将改动的组件push到dirtyComponent中,最后执行close方法执行flushBatchedUpdate方法再把isBatchingUpdates重置为false。在flushBatchedUpdates方法中事务执行runBatchedUpdates方法将dirtyComponent中的component依次(先父组件在子组件的顺序)进行更新操作。这里具体的更新的过程看流程图就可以理解了,需要注意的是updateChildren方法这个方法是virtual DOM的Diff算法的核心代码,作用就是根据更新前后组件的不同进行有效的更新,具体的部分,之后单独的文章再介绍。
在更新的过程中需要注意的一个方法是_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;
}
复制代码
可以看到当setState传入的是函数的时候,函数被调用的时候的传入的参数是merge了已经遍历的queue的state的nextState,如果传入的不是函数则直接merge state至nextState。这也解释了,为什么用回调函数的形式使用setState的时候可以解决state是按照顺序最新的state了。
从流程图可以看到在保证组件更新完毕后会将setState中传入的callback按照顺序依次push到事务的callback queue队列中,在事务结束的时候close方法中notifyAll就是执行这些callbacks,这样保证了回调函数是在组件完全更新完成后执行的,也就是setState的回调函数传入的state是更新后的state的原因。
在了解了以上的组件更新的流程后,可以看一个场景,栗子如下:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
import React, { Component } from 'react';
import Hello from './Hello';
class App extends Component {
constructor(props) {
super(props);
this.state = {
appText: 'hello App',
helloText: 'heiheihei'
};
}
handleAppClick = () => {
console.log('App is clicked ~');
this.setState({
appText: 'App is clicked ~'
});
}
render() {
const { appText, helloText } = this.state;
console.log('render App');
return (
<div className="app-container">
<div
onClick={this.handleAppClick}
>{appText}</div>
<Hello
text={helloText}
handleAppClick={this.handleAppClick}
/>
</div>
);
}
}
export default App;
import React, { Component } from 'react';
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
text: 'hello Hello'
};
}
componentWillReceiveProps(nextProps) {
this.setState({
text: nextProps.text + '~'
});
}
handleClick = () => {
this.setState({
text: 'Hello is clicked ~'
});
this.props.handleAppClick();
}
render() {
const { text } = this.state;
console.log('render Hello');
return (
<div>
<div
onClick={this.handleClick}
style={{ color: '#e00' }}
>{text}</div>
</div>
);
}
}
export default Hello;
复制代码
点击
hello Hello
复制代码
后组件的渲染如下,可以看到父组件到子组件按顺序更新了一次:
render App
render Hello
复制代码
而不是:
render Hello
render App
render Hello
复制代码
批量更新的时候组件的顺序由:
dirtyComponents.sort(mountOrderComparator);
复制代码
处理的。
到这里你需要知道这个结果产生的原因在于不是只有setState的调用栈会改变isBatchingUpdates的值
回顾《React事件机制》的流程图可以知道事件的统一回调函数dispatchEvent调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理,也就是说点击事件的处理本身就是在一个大的事务中,在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中,在事务结束的时候按照上述的流程进行批量更新,然后将批量执行关闭结束事务。
事务的机制在这里也保证了React更新的效率,此外在更新组件的时候的virtual DOM的Diff算法也起到很大的作用,这个在后续的文章再介绍。
参考资料