setState 的更新机制
一、setState 基本用法
setState(updater, [callback])
一个组件的显示形态可以由数据状态和外部参数决定,数据状态就是 state,修改 state 的值需要通过 setState
来改变,从而达到更新组件内部数据的作用:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
message: 111
}
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={(e) => this.changeText()}>点击修改 state</button>
</div>
)
}
changeText() {
this.setState({
message: 222
})
}
}
通过点击按钮触发 onclick 事件,执行 this.setState
方法更新 state 状态,然后重新执行 render 函数,使页面的视图更新。
如果直接修改 state 的状态:
changeText() {
this.state.message = 222
}
页面并不会有任何反应,但 state 的状态是已发生改变,这是因为 React 不像 vue2 中调用 Object.defineProperty
数据响应式或 Vue3 调用 Proxy
监听数据的变化,必须通过 setState
方法来告知 React 组件 state 已经发生了改变。
关于 state 方法的定义是从 React.Component
中继承,定义源码如下:
Component.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.enqueueSetState(this, partialState, callback, 'setState')
}
可以看到 setState
第一个参数可以是个对象,或是个函数,而第二个参数是一个回调函数,用于实时获取更新后的数据。
二、setState 更新类型
在使用 setState
更新数据时,setState
的更新类型分为两种:
- 异步更新
- 同步更新
1、异步更新
举个例子:
changeText() {
this.setState({
message: 222
})
console.log(this.state.message); // 111
}
可以看到,最终打印结果为 111,并不能在执行完 setState
之后立刻拿到最新的 state 结果。
如果想立刻获取更新后的值,可以在 setState
第二个参数的回调函数中获取:
changeText() {
this.setState({
message: 222
}, () => {
console.log(this.state.message) // 222
});
}
2、同步更新
举个例子:
changeText() {
setTimeout(() => {
this.setState({
message: 222
});
console.log(this.state.message) // 222
}, 0);
}
可以看到更新是同步的。setTimeout
中去 setState
可以在合成事件、钩子函数、原生事件中使用,基于 event loop 的模型下, setTimeout
中里去 setState
总能拿到最新的 state 值。
再举个原生 DOM 事件的例子:
componentDidMount() {
const btn = document.getElementById("btn");
btn.addEventListener('click', () => {
this.setState({
message: 222
});
console.log(this.state.message) // 222
})
}
3、小结
- 在组件生命周期函数或 React 合成事件(onClick、onChange)中,
setState
是异步的; - 在 setTimeout 或原生 DOM 事件中,
setState
是同步的。
三、setState 批量更新
举个例子,设原来 state.count 的值为 0:
handleClick = () => {
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
}
点击按钮触发上面的方法,打印结果都是 1,页面显示 count 的值为 2。
这是因为对同一个值进行多次 setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行结果,上面例子等价于:
Object.assign(
previousState,
{ index: state.count + 1 },
{ index: state.count + 1 },
...
)
由于后面的数据会覆盖前面的更改,所以最终只加了一次。
再举个例子:
class Test extends React.Component {
state = {
count: 0
}
componentDidMount() {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 输出
}, 0)
}
render() {
return null
}
}
- 首先第一次和第二次的
console.log
都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为 0; - 而在
setTimeout
中的console.log
处于原生事件中,所以会同步的处理再输出结果,但要注意虽然 count 在前面经过了两次的 this.state.count + 1,但每次获取的 this.state.count 都是初始化时的值,也就是 0; - 所以此时 count 是 1,那么后续在 setTimeout 中的输出则是 2 和 3。
所以输出 0,0,2,3
如果是下一个 state 依赖前一个 state 的话,建议给 setState
的参数传入一个 function,如下:
onClick = () => {
this.setState((prevState, props) => {
return { count: prevState.count + 1 }
})
this.setState((prevState, props) => {
return { count: prevState.count + 1 }
})
}
而在 setTimeout
或者原生 DOM 事件中,由于是同步的操作,所以不会出现覆盖现象。
四、调用 setState 发生了什么
setState
设置 state 数据时的流程图:
1、setState 过程
下面源码来对每一步进行分析,首先是 setState
入口函数:
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState)
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState')
}
}
入口函数在这充当了一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState
这个方法。
2、enqueueSetState 过程
enqueueSetState: function (publicInstance, partialState) {
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
这里 enqueueSetState
做了两件事:
- 将新的 state 放进组件的状态队列里;
- 用 enqueueUpdate 来处理将要更新的实例对象。
3、enqueueUpdate 过程
function enqueueUpdate(component) {
ensureInjected()
// 注意这一句是问题的关键,isBatchingUpdates 标识着当前是否处于批量创建/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component)
return
}
// 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
dirtyComponents.push(component)
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1
}
}
这个 enqueueUpdate
引出了一个关键的对象 —— batchingStrategy
,该对象所具备的 isBatchingUpdates
属性直接决定了当下是要走更新流程,还是应该排队等待;其中的 batchedUpdates
方法更是能够直接发起更新流程。由此可以推测,batchingStrategy
或许正是 React 内部专门用于管控批量更新的对象。
4、batchingStrategy 过程
var ReactDefaultBatchingStrategy = {
// 全局唯一的锁标识
isBatchingUpdates: false,
// 发起更新动作的方法
batchedUpdates: function (callback, a, b, c, d, e) {
// 缓存锁变量
var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates
// 把锁“锁上”
ReactDefaultBatchingStrategy.isBatchingUpdates = true
if (alreadyBatchingStrategy) {
callback(a, b, c, d, e)
} else {
// 启动事务,将 callback 放进事务里执行
transaction.perform(callback, null, a, b, c, d, e)
}
}
}
batchingStrategy
对象可以理解为它是一个 “锁管理器”。
这里的锁,是指 React 全局唯一的 isBatchingUpdates
变量,isBatchingUpdates
的初始值是 false,意味着当前并未进行任何批量更新操作。每当 React 调用 batchedUpdate
去执行更新动作时,会先把这个锁给锁上(置为 true),表明现在正处于批量更新过程中。当锁被锁上时,任何需要更新的组件都只能暂时进入 dirtyComponents
中排队等候下一次的批量更新,而不能随意 插队。此处体现任务锁的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
五、总结
1、state 可以直接修改吗
在类组件的构造函数中可以直接通过 this.state
来修改 state
的值。
2、setState 是同步还是异步更新
setState
在组件生命周期函数或 React 合成事件中是“异步”的,在原生事件和 setTimeout、setInterval 中是同步的;
另外,setState
的异步不是说内部由异步代码实现,其本身执行的过程和代码都是同步的,只是组件生命周期函数与合成事件调用顺序在更新之前,导致在组件生命周期函数与合成事件中没法立刻拿到更新后的值,形式了所谓的异步,可以通过 setState
的第二个参数 callback 拿到更新后的结果;
3、setState 判断同步异步的原理
在 setState
源码中,通过 isBatchingUpdates
来判断 setState
是先存进 state
队列还是直接更新,如果值为 true(组件生命周期函数或 React 合成事件中)则执行异步操作,为 false(原生事件和 setTimeout、setInterval 中)则直接更新。
4、setState 批量更新策略
setState
的批量更新策略建立在异步更新的基础上,在原生事件和 setTimeout 中不会触发批量更新,在异步更新中如果对同一个值调用多次 setState
,则 setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果对多个不同的值同时调用 setState
,则更新时会对其进行合并批量更新。具体例子见上面