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

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

创新互联成立10年来,这条路我们正越走越好,积累了技术与客户资源,形成了良好的口碑。为客户提供网站设计、成都网站制作、网站策划、网页设计、域名与空间、网络营销、VI设计、网站改版、漏洞修补等服务。网站是否美观、功能强大、用户体验好、性价比高、打开快等等,这些对于网站建设都非常重要,创新互联通过对建站技术性的掌握、对创意设计的研究为客户提供一站式互联网解决方案,携手广大客户,共同发展进步。

能问出这个问题,面试官应该对 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.        
  12.         
  13.           src={logo} alt="logo" 
  14.           className="App-logo" 
  15.           onClick={this.handleClick} 
  16.         /> 
  17.         

    我的关注人数:{this.state.count}

     
  18.       
 
  •     ); 
  •   } 
  •  
  • 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 的实验版本。

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

    分享文章:React中setState是一个宏任务还是微任务?
    本文网址:http://www.shufengxianlan.com/qtweb/news6/92006.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    服务器托管知识

    各行业网站