一、setState 的基本特性
1. setState 获取更新状态
在 React 控制的回调函数中,setState是异步的,所以在设置setState后,是无法立即通过this.state是获取最新状态的。如果要获取最新的状态得在setState回调中获取。
this.setState({
name: 'test'
}, ()=>{
console.log(this.state.name);
})
2. setState 合并调用
对象式setState多次调用会发生合并,就像Object.assign的对象合并,相同的key最后一个会覆盖前面的key。
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 1
})
// 类似于
Object.assign(
previousState,
{number: this.state.number + 1},
{number: this.state.number + 1}
)
// 只执行一次
{ ...previousState, number: this.state.number + 1 }
3. setState 同步更新
setstate在原生事件,setTimeout,setInterval,promise等异步操作中,state会同步更新。
当执行到 setTimeout 的时候,会将函数块放入宏任务列队里暂不执行,先去执行主进程代码块,等主进程执行完了, isBatchingUpdates 变为了 false ,导致最后去执行队列里的 setState 时候, 表现就会和原生事件一样,同步拿到最新的state的值。
4. setState 异步批量更新
在react生命周期和合成事件执行前后都有相应的钩子,分别是pre钩子和post钩子,pre钩子会调用batchedUpdate方法将isBatchingUpdates变量置为true,开启批量更新,而post钩子会将isBatchingUpdates置为false。
二、setState 执行过程
setstate方法
ReactComponent.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
// 这里的this.updater就是ReactUpdateQueue
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
setState函数有两个参数:
第一个参数是需要修改的setState对象,或者是函数。
第二个参数是修改之后的回调函数。
这里的partialState会产生新的state以一种Object.assgine()的方式跟旧的state进行合并。
enqueueSetState
enqueueSetState: function (publicInstance, partialState) {
// 获取当前组件的instance
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 将要更新的state放入一个数组里
// _pendingStateQueue(待更新队列) 与 _pendingCallbacks(更新回调队列)
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// 将要更新的component instance也放在一个队列里
enqueueUpdate(internalInstance);
}
enqueueSetState 主要任务:
1、将新的state放进数组里
2、用enqueueUpdate来处理将要更新的实例对象
enqueueUpdate
function enqueueUpdate(component) {
// 如果没有处于批量创建/更新组件的阶段,则处理update state事务
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
dirtyComponents.push(component);
}
由这段代码可以看到,当前如果正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在dirtyComponent里,所以不是每一次的setState都会更新组件。
batchingStrategy
var ReactDefaultBatchingStrategy = {
// 用于标记当前是否出于批量更新
isBatchingUpdates: false,
// 当调用这个方法时,正式开始批量更新
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// 如果当前事务正在更新过程在中,则调用callback,既enqueueUpdate
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 否则执行更新事务
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
这里注意两点:
1、如果当前事务正在更新过程中,则使用enqueueUpdate将当前组件放在dirtyComponent里。
2、如果当前不在更新过程的话,则执行更新事务。
transaction
简单说明一下transaction对象,它暴露了一个perform的方法,用来执行anyMethod,在anyMethod执行的前,需要先执行所有wrapper的initialize方法,在执行完后,要执行所有wrapper的close方法,就辣么简单。
在ReactDefaultBatchingStrategy.js,tranction 的 wrapper有两个 FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
这两个wrapper的initialize都没有做什么事情,但是在callback执行完之后,RESET_BATCHED_UPDATES 的作用是将isBatchingUpdates置为false, FLUSH_BATCHED_UPDATES 的作用是执行flushBatchedUpdates。
里面会循环所有dirtyComponent,调用updateComponent来执行所有的生命周期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最后实现组件的更新。
整体流程
- batchedUpdates 函数让 isBatchingUpdates = true
- 函数中调用setState
- setState 设置 enqueueSetState 和 enqueueCallback
- enqueueSetState 调用 enqueueUpdate
待更新的state放进调用component的更新队列中
enqueueUpdate 调用 component(待更新的component 放进一个更新 队列) - enqueueUpdate
判断isBatchingUpdates
false 执行batchingStrategy.batchedUpdates
true 将component放进dirtyComponents数组 - batchingStrategy.batchedUpdates
将当前的 isBatchingUpdates 设置为true
判断原先的 isBatchingUpdates 是否为true
false 立即执行更新事务
true 执行 enqueueUpdate - 事务执行时
isBatchingUpdates 设置为false
循环所有dirtyComponent,调用updateComponent来执行所有的生命周期方法,componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, render, componentDidUpdate 最后实现组件的更新。
三、setState 相关的问题
1. 连续使用 setState,为什么不能实时改变
state.count = 0;
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// state.count === 1,不是 3
因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,
2. 为什么要 setState,而不是直接改变this.state
因为 setState 做的事情不仅仅只是修改了 this.state 的值,另外最重要的是它会触发 React 的更新机制,会进行diff,然后将 patch 部分更新到真实 dom 里。
如果你直接 this.state 的话,state 的值确实会改,但是它不会驱动 React 重新渲染。
当调用 setState 后,React 的 生命周期函数 会依次顺序执行
- static getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
3. 为什么会出现异步的情况
为了性能优化,React 会将每次调用的 setState 放入队列中做一次性处理,在事件结束之后再产生一次重新渲染,为的就是把 Virtual DOM 和 DOM 树操作降到最小,提高应用性能。
参考链接
React setState 之后发生了什么
React setState 整理总结
深入浅出 setState 原理篇