React可以抽象的看成一个公式:UI = f(state)。函数f是React和基于React的代码,传给函数的参数就是state,最终在页面上绘制出来的UI是函数运行结果。
作为state管理的重要方法,setState至关重要,在使用过程中发现该方法的几个关键点:
1.setState不会立刻改变React组件中state的值
组件读取状态时使用this.state,更新状态使用this.setState,因为this.state终究只是一个对象,单纯修改对象意义不大,去驱动视图的更新才有意义。直接修改this.state的确能改变状态,但不会引发重新渲染。要知道setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state。 如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之前直接被修改的state,这样就无法合并了,而且实际也没有把你想要的state更新上去。因此需要用setState驱动组件更新,引发componentDidUpdate、render等一系列操作。
因为setState不会立刻修改this.state的值所以下面代码会产生不直观结果
function add() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
乍一看add函数调用时组件的count被增加了2次,实际上只增加一次,因为调用this.setState时并没有立即更改state,所以只是重复的操作原始值加1。
上面代码等于:
function increment() {
const curCount = this.state.count
this.setState({ count:curCount + 1 })
this.setState({ count:curCount + 1 })
}
curCount只是一个快照,重复的给同一个值加1,1+1就算执行一百次也只是等于2。
事实上,setState 方法与包含在其中的执行是一个很复杂的过程,它的工作除了要更动 this.state 之外,还要负责触发重新渲染,这里面要经过 React 核心 diff 算法,最终才能决定是否要进行重渲染,以及如何渲染。而且为了批次与效能的理由,多个 setState 呼叫有可能在执行过程中还需要被合并,所以它被设计以延时的来进行执行是相当合理的。
2.setState通过引发一次组件更新过程来引发重新绘制
接着上面例子继续说,state会在什么时候修改呢,这就要看setState引发的生命周期函,以下函数依次被调用。
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdata
前两个函数被调用时this.state都没有更新,直到render调用时this.state才更新。当shouldComponentUpdate函数返回false时,更新过程暂停,render也不会被调用,但此时React依然会更新this.state。简而言之,直到下一次render执行或下一次shouldComponentUpdate返回false时才会拿到更新后的this.state。
3.多次setState函数调用产生的效果会合并
因为state的更新是浅合并(Shallow Merge),使用setState对组件状态修改时,只用传入发生改变的state不用传入完整state,React会合并新的属性到原来的而组件状态中同时保留尚未更改的值。连续多次调用只会引发一次更新生命周期,因为React会把多个this.setState产生的修改放在一个队列里,累积到一定程度再引发一次更新。
4.函数式setState
setState可以接受两个参数,第一个是本次修改之前的state,另一个是当前最新props。上面例子写法如下:
function add (state,props) {
return { counter :state.count + 1}
}
修改state中counter,状态的来源是参数state而非this.state,对应的increment函数:
function increment() {
this.setState ( add )
this.setState ( add )
}
多次调用函数式setState时,React会保证每次调用add时,state都已经更新了之前的状态修改结果。当add函数被调用时,state并没有改变,要等到render函数执行之后或shouldcomponentUpdate返回false之后才改变。总体来说,使用函数式 setState,可以传递一个函数作为其参数,当执行该函数时,React 会将更新后的 state 复制一份并传递给它,这便起到了更新 state 的作用。基于上述机制,函数式 setState 便可基于前一刻的 state 来更新当前 state。
这种函数式编程思想很棒,开发者不用维护组件状态,只是把想要的状态改变之后的样子传递给React,它会去帮我们修改组件状态。流程控制的决定权交给了React因此它也能协调多个setState的状态。
源码分析:
源码分析将以总分结构描述,先看整体思路再对其中涉及到的方法进行逐个分析。ReactReconcileTransaction模块用于在组件元素挂载前后执行指定的钩子函数,特别是componentDidMount、componentDidUpdate生命周期调用方法,其次是向组件实例注入updater参数,实现setState、replaceState、forceUpdate方法。
源码:
ReactComponent.prototype.setState = function(partialState, callback) {
...
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
方法传入两个参数,partialState是新的state值,后者是回调函数。
setState会 被加入updater队列来执行它的工作,如果传入的参数有回调函数的话回调函数也会被加入updater队列。updater是构造函数传入的,在组件定义中发现updater就是updateQueue。
传入的state和回调函数被放入updater的enqueueSetState函数进行处理,看如下enqueueSetState的定义:
enqueueSetState: function(publicInstance, partialState) {
...
var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState' );
if (!internalInstance) {
return;
}
var queue = internalInstance.pendingStateQueue || (internalInstance.pendingStateQueue = []); queue.push(partialState);
enqueueUpdate(internalInstance);
},
getInternalInstanceReadyForUpdate能够获取当前组件对象,赋值给internalInstance变量,再判断当前组件对象的state更新队列是否存在,若存在,则把新state即partialState加入队列,不存在则创建该对象的更新队列。
enqueueUpdate方法源码如下:
function enqueueUpdate(component) {
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
如果batchingStrategy.isBatchingUpdates为false,执行batchedUpdates来更新队列,若值为true,将组件放入dirtyComponent中。也就是说batchingStrategy的属性告诉你当前是否处于事务之中,如果不是,enqueueUpdate将它自己放入事务去执行;反之,将component(ReactCompositeComponentWrapper实例)放入dirtyComponents数组中。
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内执行setState传入的回调函数。再判断 ReactDefaultBatchingStrategy.isBatchingUpdates的值,若为真值,执行callback回调添加脏组件;为否值,执行transaction的perform方法。 { 添加脏组件的同时,调用ReactUpdates.flushBatchedUpdates方法重绘组件,在ReactUpdates.enqueueUpdate方法内调用,添加脏组件,以及执行组件重绘。}
接下来了解一下react的事务机制。
事务就是将需要执行的方法用wrapper封装起来,再通过事务提供的perform方法执行。在perform之前,先执行所有wrapper中的initialize方法,执行完perform之后(即需要执行的方法)再执行所有的close方法。一组initialize和close称为一个wrapper。
我们知道在前面的batchingStrategy的代码中transaction.perform(callBack)实际调用的是transaction.perform(enqueueUpdate),但enqueueUpdate方法中仍然存在transaction.perform(enqueueUpdate),这样会不会造成了死循环? 为了防止死循环的产生,wrapper给出了定义。
var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};
var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// ReactUpdates.flushBatchedUpdates方法以特定钩子重绘dirtyComponents中的各组件 // 钩子包括ReactUpdatesFlushTransaction前后钩子,含组件重绘完成后的回调_pendingCallbacks // 包括ReactReconcileTransaction前后钩子,含componentDidMount、componentDidUpdate回调那就是RESET_BATCHED_UPDATES这个wrapper的作用是设置isBatchingUpdates也就是组件更新状态的值,组件有更新要求的话则设置为更新状态,更新结束后重新恢复原状态。这样做是为了避免组件的重复render,提升性能。
RESET_BATCHED_UPDATES是用于更改isBatchingUpdates的布尔值false或者true,那FLUSH_BATCHED_UPDATES的作用是什么呢?其实可以大致猜到它的作用是更新组件,先看下FLUSH_BATCHED_UPDATES.close()的实现逻辑:
var flushBatchedUpdates = function() {
...
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);
}
}
...
};
flushBatchedUpdates遍历所有的dirtyComponents,又通过事务的形式调用runBatchedUpdates方法,该方法执行updateComponent更新组件,若setState有回调函数则将回调函数存入callbackQueue队列。
updateComponent更新组件的源码太长就不贴了,其中可以看到执行了componentWillReceiveProps方法和shouldComponentUpdate方法。在shouldComponentUpdate之前,执行了_processPendingState方法,该函数该函数主要对state进行处理:1.如果更新队列为null,那么返回原来的state;2.如果更新队列有一个更新,那么返回更新值;3.如果更新队列有多个更新,那么通过for循环将它们合并;综上说明了,在一个生命周期内,在componentShouldUpdate执行之前,所有的state变化都会被合并,最后统一处理。
综上,以一个流程图直观分析: