setState 的更新是同步仍是异步,一直是人们津津乐道的话题。不过,实际上若是咱们须要用到更新后的状态值,并不须要强依赖其同步/异步更新机制。在类组件中,咱们能够经过this.setState
的第二参数、componentDidMount
、componentDidUpdate
等手段来取得更新后的值;而在函数式组件中,则能够经过useEffect
来获取更新后的状态。因此这个问题,其实有点无聊。react
不过,既然你们都这么乐于讨论,今天咱们就系统地梳理一下这个问题,主要分为两方面来讲:shell
class-component
)的更新机制function-component
)的更新机制在类组件中,这个问题的答案是多样的,首先抛第一个结论:npm
legacy
模式中,更新可能为同步,也可能为异步;concurrent
模式中,必定是异步。经过ReactDOM.render(<App />, rootNode)
方式建立应用,则为 legacy 模式,这也是create-react-app
目前采用的默认模式;bash
经过ReactDOM.unstable_createRoot(rootNode).render(<App />)
方式建立的应用,则为concurrent模式
,这个模式目前只是一个实验阶段的产物,还不成熟。app
是的,这不是玄学,咱们来先抛出结论,再来逐步解释它。dom
this.setState
时,为异步更新;this.setState
,则为同步更新;实验代码以下:异步
class StateDemo extends React.Component { constructor(props) { super(props) this.state = { count: 0 } } render() { return <div> <p>{this.state.count}</p> <button onClick={this.increase}>累加</button> </div> } increase = () => { this.setState({ count: this.state.count + 1 }) // 异步的,拿不到最新值 console.log('count', this.state.count) // setTimeout 中 setState 是同步的 setTimeout(() => { this.setState({ count: this.state.count + 1 }) // 同步的,能够拿到 console.log('count in setTimeout', this.state.count) }, 0) } bodyClickHandler = () => { this.setState({ count: this.state.count + 1 }) // 能够取到最新值 console.log('count in body event', this.state.count) } componentDidMount() { // 本身定义的 DOM 事件,setState 是同步的 document.body.addEventListener('click', this.bodyClickHandler) } componentWillUnmount() { // 及时销毁自定义 DOM 事件 document.body.removeEventListener('click', this.bodyClickHandler) } }
要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。函数
首先咱们来看看 setState 的主流程:this
this.setState(newState)
;newState
会存入 pending 队列;batchUpdate
;batchUpdate
,则将组件先保存在所谓的脏组件dirtyComponents
中;若是不是batchUpdate
,那么就遍历全部的脏组件,并更新它们。由此咱们能够断定:所谓的异步更新,都命中了batchUpdate
,先保存在脏组件中就完事;而同步更新,老是会去更新全部的脏组件。code
很是有意思,看来是否命中batchUpdate
是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate
,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?
这就涉及到一个颇有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些咱们看不到的东西,如今咱们经过魔法让其显现出来:
increase = () => { // 开始:默认处于bashUpdate // isBatchingUpdates = true this.setState({ count: this.state.count + 1 }) console.log('count', this.state.count) // 结束 // isBatchingUpdates = false }
increase = () => { // 开始:默认处于bashUpdate // isBatchingUpdates = true setTimeout(() => { // 此时isBatchingUpdates已经设置为了false this.setState({ count: this.state.count + 1 }) console.log('count in setTimeout', this.state.count) }, 0) // 结束 // isBatchingUpdates = false }
当 react 执行咱们所书写的函数时,会默认在首位设置isBatchingUpdates
变量。看到其中的差别了吗?当 setTimeout 执行其回调时,isBatchingUpdates
早已经在同步代码的末尾被置为false
了,因此没命中batchUpdate
。
那自定义 DOM 事件又是怎么回事?代码依然以下:
componentDidMount() { // 开始:默认处于bashUpdate // isBatchingUpdates = true document.body.addEventListener("click", () => { // 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了 this.setState({ count: this.state.count + 1, }); console.log("count in body event", this.state.count); // 能够取到最新值。 }); // 结束 // isBatchingUpdates = false }
咱们能够看到,当componentDidMount
跑完时,isBatchingUpdates
已经设置为false
了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates
固然也是false
,不会命中batchUpdate
机制。
总结:
this.setState
是同步仍是异步,关键就是看可否命中batchUpdate
机制isBatchingUpdates
是true
仍是false
batchUpdate
的场景包括:生命周期和其调用函数、React中注册的事件和其调用函数。总之,是React能够“管理”的入口,关键是“入口”。这里要注意一点:React去加isBatchingUpdate的行为不是针对“函数”,而是针对“入口”。好比setTimeout、setInterval、自定义DOM事件的回调等,这些都是React“管不到”的入口,因此不会去其首尾设置isBatchingUpdates变量。
由于这个东西只在实验阶段,因此要开启 concurrent 模式,一样须要将 react 升级为实验版本,安装以下依赖:
npm install react@experimental react-dom@experimental
其余代码不用变,只更改 index 文件以下:
- ReactDOM.render(<App />, document.getElementById('root')); + ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);
则能够发现:其更新都是异步的,在任何状况下都是如此。
在函数式组件中,咱们会这样定义状态:
const [count, setCount] = useState(0)
这时候,咱们发现当咱们不管在同步函数仍是在异步回调中调用 setCount 时,打印出来的 count 都是旧值,这时候咱们会说:setCount 是异步的。
const [count, setCount] = useState(0); // 直接调用 const handleStrightUpdate = () => { setCount(1); console.log(count); // 0 }; // 放在setTimeout回调中 const handleSetTimeoutUpdate = () => { setTimeout(() => { setCount(1); console.log(count); // 0 }); };
setCount 是异步的,这确实没错,可是产生上述现象的缘由不仅是异步更新这么简单。缘由主要有如下两点:
1,调用 setCount 时,会作合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,而后触发重渲染(re-renders
),从这个角度上来讲,setCount
确实是一个异步操做;
2,函数式的capture-value
特性决定了console.log(count)
语句打印的始终是一个只存在于当前帧的常量,因此就算不管 setCount 是否是同步的,这里都会打印出旧值。