React 中 setState 是一个宏任务还是微任务?

开发 前端
Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。在 Concurrent 模式中,渲染不是阻塞的。

[[414970]]

最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。

能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React ??。

面试官的问法是否正确?

面试官的问题是,setState 是一个宏认为还是微任务,那么在他的认知里,setState 肯定是一个异步操作。为了判断 setState 到底是不是异步操作,可以先做一个实验,通过 CRA 新建一个 React 项目,在项目中,编辑如下代码:

  1. import React from 'react'
  2. import logo from './logo.svg'
  3. import './App.css'
  4.  
  5. class App extends React.Component { 
  6.   state = { 
  7.     count: 1000 
  8.   } 
  9.   render() { 
  10.     return ( 
  11.       <div className="App"
  12.         <img 
  13.           src={logo} alt="logo" 
  14.           className="App-logo" 
  15.           onClick={this.handleClick} 
  16.         /> 
  17.         <p>我的关注人数:{this.state.count}</p> 
  18.       </div> 
  19.     ); 
  20.   } 
  21.  
  22. export default App; 

页面大概长这样:

上面的 React Logo 绑定了一个点击事件,现在需要实现这个点击事件,在点击 Logo 之后,进行一次 setState 操作,在 set 操作完成时打印一个 log,并且在 set 操作之前,分别添加一个宏任务和微任务。代码如下:

  1. handleClick = () => { 
  2.   const fans = Math.floor(Math.random() * 10) 
  3.   setTimeout(() => { 
  4.     console.log('宏任务触发'
  5.   }) 
  6.   Promise.resolve().then(() => { 
  7.     console.log('微任务触发'
  8.   }) 
  9.   this.setState({ 
  10.     count: this.state.count + fans 
  11.   }, () => { 
  12.     console.log('新增粉丝数:', fans) 
  13.   }) 

很明显,在点击 Logo 之后,先完成了 setState 操作,然后再是微任务的触发和宏任务的触发。所以,setState 的执行时机是早于微任务与宏任务的,即使这样也只能说它的执行时机早于 Promise.then,还不能证明它就是同步任务。

  1. handleClick = () => { 
  2.   const fans = Math.floor(Math.random() * 10) 
  3.   console.log('开始运行'
  4.   this.setState({ 
  5.     count: this.state.count + fans 
  6.   }, () => { 
  7.     console.log('新增粉丝数:', fans) 
  8.   }) 
  9.   console.log('结束运行'

这么看,似乎 setState 又是一个异步的操作。主要原因是,在 React 的生命周期以及绑定的事件流中,所有的 setState 操作会先缓存到一个队列中,在整个事件结束后或者 mount 流程结束后,才会取出之前缓存的 setState 队列进行一次计算,触发 state 更新。只要我们跳出 React 的事件流或者生命周期,就能打破 React 对 setState 的掌控。最简单的方法,就是把 setState 放到 setTimeout 的匿名函数中。

  1. handleClick = () => { 
  2.   setTimeout(() => { 
  3.     const fans = Math.floor(Math.random() * 10) 
  4.     console.log('开始运行'
  5.     this.setState({ 
  6.       count: this.state.count + fans 
  7.     }, () => { 
  8.       console.log('新增粉丝数:', fans) 
  9.     }) 
  10.     console.log('结束运行'
  11.   }) 

所以,setState 就是一次同步行为,根本不存在面试官的问题。

React 是如何控制 setState 的 ?

前面的案例中,setState 只有在 setTimeout 中才会变得像一个同步方法,这是怎么做到的?

  1. handleClick = () => { 
  2.   // 正常的操作 
  3.   this.setState({ 
  4.     count: this.state.count + 1 
  5.   }) 
  6. handleClick = () => { 
  7.   // 脱离 React 控制的操作 
  8.   setTimeout(() => { 
  9.     this.setState({ 
  10.       count: this.state.count + fans 
  11.     }) 
  12.   }) 

先回顾之前的代码,在这两个操作中,我们分别在 Performance 中记录一次调用栈,看看两者的调用栈有何区别。

正常操作

脱离 React 控制的操作

在调用栈中,可以看到 Component.setState 方法最终会调用 enqueueSetState 方法,而 enqueueSetState 方法内部会调用 scheduleUpdateOnFiber 方法,区别就在于正常调用的时候,scheduleUpdateOnFiber 方法内只会调用 ensureRootIsScheduled ,在事件方法结束后,才会调用 flushSyncCallbackQueue 方法。而脱离 React 事件流的时候,scheduleUpdateOnFiber 在 ensureRootIsScheduled 调用结束后,会直接调用 flushSyncCallbackQueue 方法,这个方法就是用来更新 state 并重新进行 render 。

  1. function scheduleUpdateOnFiber(fiber, lane, eventTime) { 
  2.   if (lane === SyncLane) { 
  3.     // 同步操作 
  4.     ensureRootIsScheduled(root, eventTime); 
  5.     // 判断当前是否还在 React 事件流中 
  6.     // 如果不在,直接调用 flushSyncCallbackQueue 更新 
  7.     if (executionContext === NoContext) { 
  8.       flushSyncCallbackQueue(); 
  9.     } 
  10.   } else { 
  11.     // 异步操作 
  12.   } 

上述代码可以简单描述这个过程,主要是判断了 executionContext 是否等于 NoContext 来确定当前更新流程是否在 React 事件流中。

众所周知,React 在绑定事件时,会对事件进行合成,统一绑定到 document 上( react@17 有所改变,变成了绑定事件到 render 时指定的那个 DOM 元素),最后由 React 来派发。

所有的事件在触发的时候,都会先调用 batchedEventUpdates$1 这个方法,在这里就会修改 executionContext 的值,React 就知道此时的 setState 在自己的掌控中。

  1. // executionContext 的默认状态 
  2. var executionContext = NoContext; 
  3. function batchedEventUpdates$1(fn, a) { 
  4.   var prevExecutionContext = executionContext; 
  5.   executionContext |= EventContext; // 修改状态 
  6.   try { 
  7.     return fn(a); 
  8.   } finally { 
  9.     executionContext = prevExecutionContext; 
  10.   // 调用结束后,调用 flushSyncCallbackQueue 
  11.     if (executionContext === NoContext) { 
  12.       flushSyncCallbackQueue(); 
  13.     } 
  14.   } 

所以,不管是直接调用 flushSyncCallbackQueue ,还是推迟调用,这里本质上都是同步的,只是有个先后顺序的问题。

未来会有异步的 setState

如果你有认真看上面的代码,你会发现在 scheduleUpdateOnFiber 方法内,会判断 lane 是否为同步,那么是不是存在异步的情况?

  1. function scheduleUpdateOnFiber(fiber, lane, eventTime) { 
  2.   if (lane === SyncLane) { 
  3.     // 同步操作 
  4.     ensureRootIsScheduled(root, eventTime); 
  5.     // 判断当前是否还在 React 事件流中 
  6.     // 如果不在,直接调用 flushSyncCallbackQueue 更新 
  7.     if (executionContext === NoContext) { 
  8.       flushSyncCallbackQueue(); 
  9.     } 
  10.   } else { 
  11.     // 异步操作 
  12.   } 

React 在两年前,升级 fiber 架构的时候,就是为其异步化做准备的。在 React 18 将会正式发布 Concurrent 模式,关于 Concurrent 模式,官方的介绍如下。

什么是 Concurrent 模式?

Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。在 Concurrent 模式中,渲染不是阻塞的。它是可中断的。这改善了用户体验。它同时解锁了以前不可能的新功能。

现在如果想使用 Concurrent 模式,需要使用 React 的实验版本。

本文转载自微信公众号「自然醒的笔记本」,可以通过以下二维码关注。转载本文请联系自然醒的笔记本公众号。

 

责任编辑:武晓燕 来源: 自然醒的笔记本
相关推荐

2023-04-06 00:22:19

JavaScrip任务开发

2021-12-04 22:05:41

网页任务 Performanc

2022-06-13 06:20:42

setStatereact18

2020-12-29 08:21:03

JavaScript微任务宏任务

2021-07-24 11:15:19

开发技能代码

2022-06-13 10:24:47

宏任务微任务前端

2021-02-02 11:02:20

React任务饥饿行为优先级任务

2018-07-17 15:15:33

任务调度系统

2023-03-01 09:39:40

调度系统

2012-12-24 13:25:59

微信App

2017-04-12 11:15:52

ReactsetState策略

2021-08-23 15:14:09

Linuxat命令任务

2024-03-14 09:07:05

刷数任务维度后端

2021-02-02 14:55:48

React前端高优先

2022-09-16 08:32:17

Reduxreact

2021-06-29 09:47:34

ReactSetState机制

2023-04-14 08:48:57

AutoGPT工具人工智能

2023-11-13 07:37:36

JS面试题线程

2021-01-18 08:24:51

JavaScriptMicrotask微任务

2024-09-10 08:10:00

信号设计id
点赞
收藏

51CTO技术栈公众号