相信很多小伙伴面试的时候,都经历过这么一个很是 “无聊” 的问题?但是,也没办法,就是很多面试官在面试的时候喜欢问。为什么说这个问题很无聊呢?因为我们在实际的开发中,根本就可以不用依赖这个特性,不需要管他是异步执行还是同步执行的。因为在如果你需要获取更新后的值,React
本来就提供了方法给开发者去获取。 Class Component
中,我们可以在生命周期 ComponentDidUpdate
中获取到更新后的值,而对于 Functional Component
,我们也可以在 useEffect
的回调中去处理获取更新后state的逻辑操作。所以,setState
是同步还是异步,好像也不会影响我们正常的开发。不过,既然聊到了这个,我们不妨试着从不同的角度来看一下setState
在 React
中到底是怎么执行的?
React 中的模式
相信熟悉 React 的小伙伴肯定是知道,React 是有多种模式的。如果你还不知道的话,那… 也可以考虑去 React
官网好好学习学习。
原因: 官网有介绍到 React
中是有以下三种模式的,而至于为什么会有这么多重模式嘛,官网上也有详细的说明。大致的原因就是因为版本的更新迭代,而官方又不想直接放弃对之前版本的维护,所以就会出现多个模式啦。
- legacy 模式: ReactDOM.render(, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
- blocking 模式: ReactDOM.createBlockingRoot(rootNode).render()。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
- concurrent 模式: ReactDOM.createRoot(rootNode).render()。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。
不是说 setState
吗?怎么又扯到模式上去了呢?聪明的小伙伴们肯定已经猜到了,因为setState 的表现在不同的模式下,表现是有些差异的。因为在不同的模式下,我们创建的更新会有不同的优先级,并且更新的过程也是可以被打断的。下面我们一起来看看,到底都有哪些差异吧!
Legacy 模式
说明:由于blocking 模式和concurrent 模式都还处于实验中,我也不是很了解,所以就不做过多的解释
我们通过 ReactDOM.render()
方法创建的应用,它的模式就是Legacy,在Legacy 模式下触发的更新,它的状态的更新是异步的。Talk is cheap, show you code~~
# index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const rootElement = document.getElementById("root")
ReactDOM.render(<App />, rootElement)
# App.js
import React, { Component } from 'react'
export default class App extends Component {
state = {
count: 0
}
updateCount = () => {
console.log(this.state.count)
this.setState({ num: this.state.num+1 })
console.log(this.state.count)
}
render() {
return <p onClick={this.updateCount}>{this.state.count}</p>
}
}
上面的代码相信小伙伴们都是不能再熟悉了。很多小伙伴开发的时候就是基于这种模式的,经常是 setState
之后就想要拿(这种多半是习惯了 Vue
开发的)。但是结果,我相信小伙伴们都是很清楚的了,不管是 setState
之前,还是之后,我们拿到的都是更新之前
的数据。而造成这一现象的原因是什么呢?
原因
在React 中有一个性能优化的做法 – batchedUpdates
,我们都知道,setState
之后是要触发页面的更新或者重新渲染的,那你要是在很短的时间内连续的触发,React
就要连续的去做更新,这就很像我们做输入框搜索的节流防抖的那种效果,我把你几次 setState
合并到一起,一块去做更新,不就可以节省很多不必要的性能浪费吗?
解决方法
可以把 this.setState()
方法的执行放到 setTimeout
中就可以啦。
这时候可能就有小伙伴要问了,为什么放在setTimeout 中,它就能同步执行了呢?那我们就要聊一聊源码了。在 React
的react-reconciler
中的 ReactFiberWorkLoop.old.js
的文件中,有一个batchedUpdates 的方法。有兴趣的小伙伴也可以去 github
拉以下 React
源码回来研究研究。
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
// 保存开始的状态
const prevExecutionContext = executionContext;
// 在这里它把 executionContext 附加上 BatchedContext 的这个标志
// 如果带有 BatchedContext 这个标志,React 就会把它当成批处理,去异步更新。
// 如果我们的setState是异步执行的,当我们执行setState的时候,就不会存在BatchedContext了(executionContext是全局变量)
executionContext |= BatchedContext;
try {
// 这里的fn,其实就是上面那个计数器代码中的updataCount
return fn(a);
} finally {
// 这行完fn 以后,他又把 executionContext 放会到原来的状态
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
每次调度更新的时候,React 都会调用一个函数 scheduleUpdateOnFiber
,在scheduleUpdateOnFiber中,有这么一段逻辑。就是说,如果 executionContext
是 NoContext
,React就会执行 flushSyncCallbackQueue()
,也就是同步的执行更新。
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
schedulePendingInteractions(root, lane);
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
// 将同步执行代码
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
其实说到这个,我想小伙伴们就应该清楚了setState 在react 中是怎么执行的了吧。如果还不明白的话,可以静下心来,再认真的阅读一次,相信你也能够很清楚的理解setState的。
补充
最后,做个小小的补充,如果是在 concurrent 模式
下,也就是使用 ReactDOM.unstable_createRoot(rootElement).render(<App />)
创建应用的话,setTimeout 就不会生效了。因为在上面的代码我们可以看到,要走到 (executionContext === NoContext)
的一个大前提就是 (lane === SyncLane)
,也就是说,当前更新的优先级是同步的优先级,而我们通过 ReactDOM.render(<App />, rootElement)
创建的应用所触发的更新都是同步的优先级。不过 concurrent 模式
现在还处在实验阶段,一般很少有人用。