React 修改 state 方法有两种:
1、构造函数里修改 state
,只需要直接操作 this.state
即可, 如果在构造函数里执行了异步操作,就需要调用 setState
来触发重新渲染。
2、在其余的地方需要改变 state 的时候只能使用 setState
,这样 React 才会触发 UI 更新,如果在其余地方直接修改 state
中的值,会报错:
this.state.counter += 1 // Do not mutate state directly. Use setState()
React 不能直接通过修改 state
的值来使界面发生更新,原因如下:
1、React 并没有实现类似于 Vue2 的 Object.defineProperty
或者 Vue3 的 Proxy
的方式来监听数据的变化;
2、直接修改 state
时 React 并不知道数据发生了变化,需通过 setState
来告知 React 数据已经发生了变化;
先来看 React 官网对于 setState
的说明:
将 setState() 认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React 可能会推迟它,稍后会一次性更新这些组件。React 不会保证在 setState 之后,能够立刻拿到改变的结果。
以上说明 setState
本身并不是异步的,只是因为 React 的性能优化机制将其体现为异步。
来看以下这样一段代码执行:
for (let i = 0; i < 100; i++) {
this.setState({ num: this.state.num + 1 });
}
如果此时 setState
同步执行,那么这个组件会被重新渲染 100 次,非常耗性能。
总结:
如果所有 setState
是同步的,意味着每执行一次 setState
时(一个方法中可能多次调用 setState
),都重新 vnode diff + dom
修改,这对性能来说是极为不好的。
如果是异步,则可以把一个同步代码中的多个 setState
合并成一次组件更新。
在组件生命周期或 React 合成事件中,setState
是异步的,例如:
state = {
number: 1
};
componentDidMount(){
this.setState({ number: 3 })
console.log(this.state.number) // 1
}
上述例子调用了 setState
后输出 number
的值,仍为 1,这看似异步的行为,实则是因为 React 框架本身的性能机制所导致的。
因为每次调用 setState
都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少 re-render 调用。
在回调函数、setTimeout
或原生 dom
事件中,setState
是同步的;
setState
第二个参数提供回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据,例如:
state = {
number: 1
};
componentDidMount(){
this.setState({ number: 3 }, () => {
console.log(this.state.number) // 3
})
}
上述例子调用了 setState
后输出 number
的值就为 3 了,我们也就实时的获取到了最新的数据。
上面我们讲到了,setState
本身并不是一个异步方法,其之所以会表现出一种异步的形式,是因为 React 框架本身的一个性能优化机制。
那么基于这一点,如果我们能够越过 React 的机制,是不是就可以令 setState
以同步的形式体现了呢~
state = {
number: 1
};
componentDidMount(){
setTimeout(() => {
this.setState({ number: 3 })
console.log(this.state.number) // 3
}, 0)
}
上述例子调用了 setState
后输出 number
的值也是最新的数据 3,这也完美的印证了我们的猜想是正确的。
上面已经印证了避过 React 的机制,可以同步获取到更新之后的数据,那么除了 setTimeout
外,在原生事件中也是可以的:
state = {
number: 1
};
componentDidMount() {
document.body.addEventListener('click', this.changeVal, false);
}
changeVal = () => {
this.setState({
number: 3
})
console.log(this.state.number) // 3
}
经过实践,同样这种方法也是可行的。
setState
设置 state 数据时的流程图:
下面来看下每一步的源码,首先是 setState
入口函数:
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.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
做了两件事:
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 内部专门用于管控批量更新的对象。
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 面对大量状态仍然能够实现有序分批处理的基石。
在类组件的构造函数中可以直接修改 state
,只需要直接操作 this.state
即可。
setState
并不是单纯同步 / 异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 和原生 dom
事件等情况下,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。
在 setState
源码中,通过 isBatchingUpdates
来判断 setState
是先存进 state
队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
① 在 React 可以控制的地方,isBatchingUpdates
就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
② 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener
、setTimeout
、setInterval
等事件中,就只能同步更新。
以下代码输出什么?
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;
}
};
所以输出 0,0,2,3