api解析: setState(updater, [callback])
updater: 更新数据 FUNCTION/OBJECT
callback: 更新成功后的回调 FUNCTION
// updater - Function
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
});
// update - Object
this.setState({quantity: 2})
setState的特点:
1.异步:react通常会集齐一批需要更新的组件,然后一次性更新来保证渲染的性能
2.浅合并 Objecr.assign()
setState问题与解决
举个🌰
- 在使用setState改变状态之后,立刻通过this.state去拿最新的状态
解决: componentDidUpdate或者setState的回调函数里获取
// setState回调函数
changeTitle: function (event) {
this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
// Call API with the updated value
}
- 有一个需求,需要在在onClick里累加两次,使用对象的方法更新,则只会加一次
解决: 使用updater function
onClick = () => {
this.setState({ index: this.state.index + 1 });
this.setState({ index: this.state.index + 1 });
}
// 最后解析为,后面的数据会覆盖前面的更改,所以最终只加了一次.
Object.assign(
previousState,
{index: state.index+ 1},
{index: state.index+ 1},
)
//正确写法
onClick = () => {
this.setState((prevState, props) => {
return {quantity: prevState.quantity + 1};
});
this.setState((prevState, props) => {
return {quantity: prevState.quantity + 1};
});
}
注意:
1.不要在render()函数里面写setstate(),除非你自己定制了shouldComponentUpdate方法,要不然会引起无限循环
render() {
//this.setState
return(
//...dom
)
}
2.不要给this.state直接复制
react为了实现高效render, state其实是一个队列,setState是将数据插入队列中,使用方式1直接赋值不会触发渲染, react提供了setState的实例方法可以触发render。
// 1
this.state.num = 1
// 2
this.setState({
num: this.state.num + 1
})
3.对数组和对象等引用对象操作时,使用返回新对象的方法
array: 不要使用push、pop、shift、unshift、splice可使用concat、slice、filter、扩展语法
object: Object.assgin/扩展语法
setState更新机制
如图:
图不清楚可以点击查看原图
-
partialState
:setState
传入的第一个参数,对象或函数 -
_pendingStateQueue
:当前组件等待执行更新的state
队列 -
isBatchingUpdates
:react用于标识当前是否处于批量更新状态,所有组件公用 -
dirtyComponent
:当前所有处于待更新状态的组件队列 -
transcation
:react的事务机制,在被事务调用的方法外包装n个waper
对象,并一次执行:waper.init
、被调用方法、waper.close
-
FLUSH_BATCHED_UPDATES
:用于执行更新的waper
,只有一个close
方法
2.执行过程
对照上面流程图的文字说明,大概可分为以下几步:
- 1.将setState传入的
partialState
参数存储在当前组件实例的_pendingStateQueue
中。 - 2.判断当前React是否处于批量更新状态,如果是,将当前组件标记为dirtyCompontent,并加入待更新的组件队列中。
- 3.如果未处于批量更新状态,将
isBatchingUpdates
设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。 - 4.调用事务的
waper
方法,遍历待更新组件队列依次执行更新。 - 5.执行生命周期
componentWillReceiveProps
。 - 6.将组件的state暂存队列中的
state
进行合并,获得最终要更新的state对象,并将_pendingStateQueue
置为空。 - 7.执行生命周期
shouldComponentUpdate
,根据返回值判断是否要继续更新。 - 8.执行生命周期
componentWillUpdate
。 - 9.执行真正的更新,
render
。 - 10.执行生命周期
componentDidUpdate
。
setState源码世界
相信能到这里的同学都知道了setState()是个既能同步又能异步
的方法了,那具体什么时候是同步的,什么时候是异步的?
去源码里面看实现是比较靠谱的方式。
1、如何快速查看react源码
上react的github仓库,直接clone下来
git clone https://github.com/facebook/react.git
到目前我看为止,最新的版本是16.13.1,我选了15.6.0的代码
如何切换版本?
1、找到对应版本号
2、复制15.6.0的历史记录号
3、回滚
git reset --hard 911603b
如图,成功回滚到15.6.0版本
2、setState入口 => enqueueSetState
核心原则:既然是看源码,那当然就不是一行一行的读代码,而是看核心的思想,所以接下来的代码都只会放核心代码,旁枝末节只提一下或者忽略setState的入口文件在src/isomorphic/modern/class/ReactBaseClasses.jsReact
组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
partialState
顾名思义-“部分state”,这取名,大概就是想不影响原来的state的意思吧
当调用setState
时实际上是调用了enqueueSetState
方法,我们顺藤摸瓜(我用的是vscode的全局搜索),找到了这个文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
这个文件导出了一个ReactUpdateQueue对象,“react更新队列”,代码名字起的好可以自带注释,说的就是这种大作吧,在这里注册了enqueueSetState方法
3、enqueueSetState => enqueueUpdate
先看enqueueSetState的定义
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
这里只需要关注internalInstance的两个属性:
- _pendingStateQueue:待更新队列
- _pendingCallbacks: 更新回调队列
如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue,最后执行enqueueUpdate(internalInstance)
接下来看enqueueUpdatefunction
enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
它执行的是ReactUpdates的enqueueUpdate方法
var ReactUpdates = require('ReactUpdates');
这个文件刚好就在旁边src/renderers/shared/stack/reconciler/ReactUpdates.js
。找到enqueueUpdate
方法
var ReactUpdates = {
/**
* React references `ReactReconcileTransaction` using this property in order
* to allow dependency injection.
*
* @internal
*/
ReactReconcileTransaction: null,
batchedUpdates: batchedUpdates,
enqueueUpdate: enqueueUpdate,
flushBatchedUpdates: flushBatchedUpdates,
injection: ReactUpdatesInjection,
asap: asap,
};
module.exports = ReactUpdates;
定义如下
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
这段代码对于理解setState非常重要
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
判断batchingStrategy.isBatchingUpdates。batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程,开始默认值为false
上面这句话的意思是:
如果处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中;如果不处于批量更新模式,对所有队列中的更新执行batchedUpdates方法,往下看下去就知道是用事务的方式批量的进行component的更新,事务在下面。
借用《深入React技术栈》Page167中一图
4、核心:batchedUpdates => 调用transaction
那batchingStrategy.isBatchingUpdates
又是怎么回事呢?看来它才是关键.
但是,batchingStrategy 对象并不好找,它是通过 injection 方法注入的,一番寻找,发现了 batchingStrategy 就是ReactDefaultBatchingStrategy。 src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js
具体怎么找文件,又属于另一个范畴了,我们今天只专注 setState,其他的容后再说吧
相信部分同学看到这里已经有些迷糊了,没关系,再坚持一下,旁枝末节先不管,只知道我们找到了核心方法batchedUpdates,马上要胜利了,别放弃(我第一次看也是这样熬过来的,一遍不行就两遍,大不了看多几遍又如何)
先看批量更新策略-batchingStrategy,它到底是什么
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);
}
},
};
module.exports = ReactDefaultBatchingStrategy;
终于找到了,isBatchingUpdates属性和batchedUpdates方法如果isBatchingUpdates为true,当前正处于更新事务状态中,则将Component存入dirtyComponent中,否则调用batchedUpdates处理,发起一个transaction.perform()
注:所有的 batchUpdate 功能都是通过执行各种 transaction 实现的这是事务的概念,先了解一下事务吧
5、Transaction(事务)
这一段就直接引用书本里面的概念吧,《深入React技术栈》Page169
简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。
具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。
下面这段代码应该能帮助理解
var Transaction = require('./Transaction');
// 我们自己定义的 Transaction
var MyTransaction = function() {
// do sth.
this.reinitializeTransaction();
};
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);
// before method perform
// test
// after method perform
看了上面的代码,如果还没有了解transaction.。没关系。可以看一下这篇文章,写的非常详细
React transaction完全解读
6、核心分析:batchingStrategy 批量更新策略
回到batchingStrategy:批量更新策略,再看看它的代码实现
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);
}
},
};
可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程
还记得上面说的很重要的那段代码吗
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
1、首先,点击事件的处理本身就是在一个大的事务中(这个记着就好),isBatchingUpdates已经是true了
2、调用setState()时,调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理
3、在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中;
4、在事务结束的时候才通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将批量执行关闭结束事务。
到这里我并没有顺着ReactUpdates.flushBatchedUpdates方法讲下去,这部分涉及到渲染和Virtual Dom的内容,反正你知道它是拿来执行渲染的就行了。
到这里为止,setState的核心概念已经比较清楚了,再往下的内容,暂时先知道就行了,不然展开来讲一环扣一环太杂了,我们做事情要把握核心。
到这里不知道有没有同学想起一个问题 ?
isBatchingUpdates 标志位在 batchedUpdates 发起的时候被置为 true ,那什么时候被复位为false的呢?
还记得上面的事务的close方法吗,同一个文件src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction();
// 定义复位 wrapper
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
// 定义批更新 wrapper
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;
}
});
相信眼尖的同学已经看到了,close的时候复位,把isBatchingUpdates设置为false。
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
通过原型合并,事务的close 方法,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新
到这里,我们会发现,前面所有的队列、batchUpdate等等都是为了来到事务的这一步,前面都只是批收集的工作,到这里才真正的完成了批更新的操作。
当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。
说了这么多 Transaction,关于上文提到的RESET_BATCHED_UPDATES主要用来管理isBatchingUpdates状态这句话是不是;理解更透彻了呐?
上文提到了两个wrapper:RESET_BATCHED_UPDATES和FLUSH_BATCHED_UPDATES。RESET_BATCHED_UPDATES用来管理isBatchingUpdates状态,我们前面在分析setState是否立即生效时已经讲解过了。那FLUSH_BATCHED_UPDATES用来干嘛呢?
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var flushBatchedUpdates = function () {
// 循环遍历处理完所有dirtyComponents
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
// close前执行完runBatchedUpdates方法,这是关键
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};
FLUSH_BATCHED_UPDATES会在一个transaction的close阶段运行runBatchedUpdates,从而执行update。
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
dirtyComponents.sort(mountOrderComparator);
for (var i = 0; i < len; i++) {
// dirtyComponents中取出一个component
var component = dirtyComponents[i];
// 取出dirtyComponent中的未执行的callback,下面就准备执行它了
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
var markerName;
if (ReactFeatureFlags.logTopLevelRenders) {
var namedComponent = component;
if (component._currentElement.props === component._renderedComponent._currentElement) {
namedComponent = component._renderedComponent;
}
}
// 执行updateComponent
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
// 执行dirtyComponent中之前未执行的callback
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
}
}
}
}
runBatchedUpdates循环遍历dirtyComponents数组,主要干两件事。首先执行performUpdateIfNecessary来刷新组件的view,然后执行之前阻塞的callback。下面来看performUpdateIfNecessary。
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
// receiveComponent会最终调用到updateComponent,从而刷新View
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
}
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
// 执行updateComponent,从而刷新View。这个流程在React生命周期中讲解过
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
}
}
最后惊喜的看到了receiveComponent和updateComponent吧。receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。 从而完成组件更新的整套流程。
updateComponent: function(
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext,
) {
var inst = this._instance;
invariant(
inst != null,
'Attempted to update component `%s` that has already been unmounted ' +
'(or failed to mount).',
this.getName() || 'ReactCompositeComponent',
);
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) {
inst.componentWillReceiveProps(nextProps, nextContext);
}
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(
nextProps,
nextState,
nextContext,
);
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
}
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;
}
},
_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 = Object.assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
Object.assign(
nextState,
typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial,
);
}
return nextState;
},
这一部分代码相对来说不算是很难,replace是存在是由于之前被废弃的APIthis.replaceState,我们现在不需要关心这一部分,现在我们可以回答刚开始的问题,为什么给setState传入的参数是函数时,就可以解决刚开始的例子。
Object.assign(
nextState,
typeof partial === 'function' ?
partial.call(inst, nextState, props, context) :
partial
);
如果我们传入的是对象
this.setState({value: this.state.value + 1 });
this.setState({value: this.state.value + 1})
我们现在已经知道,调用setState是批量更新,那么第一次调用之后,this.state.value的值并没有改变。两次更新的value值其实是一样的,所以达不到我们的目的。但是如果我们传递的是回调函数的形式,那么情况就不一样了,partial.call(inst, nextState, props, context)接受的state都是上一轮更新之后的新值,因此可以达到我们预期的目的。
_processPendingState在计算完新的state之后,会_performComponentUpdate:
function _performComponentUpdate(
nextElement,
nextProps,
nextState,
nextContext,
transaction,
unmaskedContext
) {
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) {
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) {
transaction.getReactMountReady().enqueue(
inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
inst
);
}
}
我们可以看到,这部分内容涉及到了几方面内容,首先在更新前调用了钩子函数componentWillUpdate,然后更新了组件的属性(props、state、context),执行函数_updateRenderedComponent,最后再次执行钩子函数componentDidUpdate。
_updateRenderedComponent
执行组件的render方法。
在文件/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
中,代码如下:
/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponent: function(transaction, context) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
var nextRenderedElement = this._renderValidatedComponent();
var debugID = 0;
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context),
);
} else {
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance, false);
var nodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
nextRenderedElement,
nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);
this._renderedComponent = child;
var nextMarkup = ReactReconciler.mountComponent(
child,
transaction,
this._hostParent,
this._hostContainerInfo,
this._processChildContext(context),
debugID,
);
this._replaceNodeWithMarkup(
oldHostNode,
nextMarkup,
prevComponentInstance,
);
}
},
/**
* Overridden in shallow rendering.
*
* @protected
*/
_replaceNodeWithMarkup: function(oldHostNode, nextMarkup, prevInstance) {
ReactComponentEnvironment.replaceNodeWithMarkup(
oldHostNode,
nextMarkup,
prevInstance,
);
},
/**
* @protected
*/
_renderValidatedComponentWithoutOwnerOrContext: function() {
var inst = this._instance;
var renderedElement;
renderedElement = inst.render();
return renderedElement;
},
到目前为止,我们已经基本介绍完了setState的更新过程,只剩一个部分没有介绍,那就是setState执行结束之后的回调函数。我们知道,setState函数中如果存在callback,则会有:
if (callback) {
this.updater.enqueueCallback(this, callback);
}
call函数会被传递给this.updater的函数enqueueCallback,然后非常类似于setState,callback会存储在组件内部实例中的_pendingCallbacks属性之中。我们知道,回调函数必须要setState真正完成之后才会调用,那么在代码中是怎么实现的。大家还记得在函数flushBatchedUpdates中有一个事务ReactUpdatesFlushTransaction:
//代码有省略
var flushBatchedUpdates = function() {
while (dirtyComponents.length) {
if (dirtyComponents.length) {
//从事务pool中获得事务实例
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
//释放实例
ReactUpdatesFlushTransaction.release(transaction);
}
//......
}
};
我们现在看看ReactUpdatesFlushTransaction的wrapper是怎么定义的:
var UPDATE_QUEUEING = {
initialize: function() {
this.callbackQueue.reset();
},
close: function() {
this.callbackQueue.notifyAll();
},
};
我们看到在事务的close阶段定义了this.callbackQueue.notifyAll(),即执行了回调函数,通过这种方法就能保证回调函数一定是在setState真正完成之后才执行的。到此为止我们基本已经解释了setState大致的流程是怎样的,但是我们还是没有回答之前的一个问题,为什么下面的两种代码会产生不同的情况:
//未按预期执行
_addValue() {
this.setState({
value: this.state.value + 1
})
this.setState({
value: this.state.value + 1
})
}
//按预期执行
_addValue() {
setTimeout(()=>{
this.setState({
value: this.state.value + 1
});
this.setState({
value: this.state.value + 1
});
},0)
}
这个问题,其实真的要追本溯源地去讲,是比较复杂的,我们简要介绍一下。在第一种情况下,如果打断点追踪你会发现,在第一次执行setState前,已经触发了一个 batchedUpdates,等到执行setState时已经处于一个较大的事务,因此两个setState都是会被批量更新的(相当于异步更新的过程,thi.state.value值并没有立即改变),执行setState只不过是将两者的partialState传入dirtyComponents,最后再通过事务的close阶段的flushBatchedUpdates方法去执行重新渲染。但是通过setTimeout函数的包装,两次setState都会在click触发的批量更新batchedUpdates结束之后执行,这两次setState会触发两次批量更新batchedUpdates,当然也会执行两个事务以及函数flushBatchedUpdates,这就相当于一个同步更新的过程,自然可以达到我们的目的,这也就解释了为什么React文档中既没有说setState是同步更新或者是异步更新,只是模糊地说到,setState并不保证同步更新。
举个🌰
如下代码:
class App extends React.Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}
render() {
return <div>{this.state.val}</div>
}
}
// 结果就为 0, 0, 2, 3
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
- setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和- 钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
所以基于上述结论,如果想要实现上述代码中 4 次 console.log 打印出来的 val 分别是1、2、3、4。可以实现如下:
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 1
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 2
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 3
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 4
}, 0);
或者
this.setState((prevState) => {
return { count: prevState.val + 1 }
})
console.log(this.state.val); // 1
this.setState((prevState) => {
return { count: prevState.val + 1 }
})
console.log(this.state.val); // 2
this.setState((prevState) => {
return { count: prevState.val + 1 }
})
console.log(this.state.val); // 3
this.setState((prevState) => {
return { count: prevState.val + 1 }
})
console.log(this.state.val); // 4
setState 干了什么
1、合成事件中的setState
react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。
按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
2、生命周期函数中的setState
整个生命周期中就是一个事物操作,所以标识位isBatchingUpdates = true,所以流程到了enqueueUpdate()时,实例对象都会加入到dirtyComponents 数组中
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
3、原生事件中的setState
原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件
原生事件绑定不会通过合成事件的方式处理,自然也不会进入更新事务的处理流程。setTimeout也一样,在setTimeout回调执行时已经完成了原更新组件流程,不会放入dirtyComponent进行异步更新,其结果自然是同步的。
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
4、setTimeout中的setState
由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。
在生命周期,根据event loop的模型,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
5、批量更新
在 setState 的时候react内部会创建一个 updateQueue ,通过 firstUpdate 、 lastUpdate 、 lastUpdate.next 去维护一个更新的队列,在最终的 performWork 中,相同的key会被覆盖,只会对最后一次的 setState 进行更新
分别执行以下代码:
componentDidMount() {
this.setState({ index: this.state.index + 1 }, () => {
console.log(this.state.index);
})
this.setState({ index: this.state.index + 1 }, () => {
console.log(this.state.index);
})
}
componentDidMount() {
this.setState((preState) => ({ index: preState.index + 1 }), () => {
console.log(this.state.index);
})
this.setState(preState => ({ index: preState.index + 1 }), () => {
console.log(this.state.index);
})
}
执行结果:
1
1
2
2
说明:
1.直接传递对象的setstate会被合并成一次
2.使用函数传递state不会被合并
批量更新中State合并机制
我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的state。
_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;
},
我们只需要关注下面这段代码:
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
如果传入的是对象,很明显会被合并成一次:
Object.assign(
nextState,
{index: state.index+ 1},
{index: state.index+ 1}
)
如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。
总结
setState流程还是很复杂的,设计也很精巧,避免了重复无谓的刷新组件。它的主要流程如下:
- enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component;
2.如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。
3.batchedUpdates发起一次transaction.perform()事务;
4.开始执行事务初始化,运行,结束三个阶段;
初始化:事务初始化阶段没有注册方法,故无方法要执行;
运行:执行setSate时传入的callback方法,一般不会传callback参数;
结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法。
5.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。