Skip to main content

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。

这是因为对同一个值进行多次 setStatesetState 的批量更新策略会对其进行覆盖,取最后一次的执行结果,上面例子等价于:

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 合成事件中是“异步”的,在原生事件和 setTimeoutsetInterval 中是同步的;

另外,setState 的异步不是说内部由异步代码实现,其本身执行的过程和代码都是同步的,只是组件生命周期函数与合成事件调用顺序在更新之前,导致在组件生命周期函数与合成事件中没法立刻拿到更新后的值,形式了所谓的异步,可以通过 setState 的第二个参数 callback 拿到更新后的结果;

3、setState 判断同步异步的原理

setState 源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true(组件生命周期函数或 React 合成事件中)则执行异步操作,为 false(原生事件和 setTimeoutsetInterval 中)则直接更新。

4、setState 批量更新策略

setState 的批量更新策略建立在异步更新的基础上,在原生事件和 setTimeout 中不会触发批量更新,在异步更新中如果对同一个值调用多次 setState,则 setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果对多个不同的值同时调用 setState,则更新时会对其进行合并批量更新。具体例子见上面