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。

这是因为对同一个值进行多次 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,则更新时会对其进行合并批量更新。具体例子见上面