zoukankan      html  css  js  c++  java
  • 重学前端 --- Promise里的代码为什么比setTimeout先执行?

    首先通过一段代码进入讨论的主题

       var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
      });
      setTimeout(()=>console.log("d"), 0)
      r.then(() => console.log("c"));
      console.log("b")
    
      // a b c d

    了解过 Promise 对象的都知道(如果还不了解,可以查看 Promise对象),Promise 新建后会立即执行,所以首先会输出a,这个没有问题。setTimeout 和 then 这两个回调函数会在本轮事件循环结束以后执行,所以第二个输出的是b,这个也没有问题,但是回过头来执行 setTimeout 和 then 方法时,setTimeout 的执行顺序明明先于 then 方法且延迟时间为0毫秒,为什么却后执行呢?是因为HTML5标准中规定setTimeout最小延迟时间不足4毫秒的仍然取值为4毫秒吗?显然不是,此处,就算把延迟时间从0改为4000毫秒,依然滞后于then 方法输出。接下来进入正题

    提示:阮一峰老师的文章 《JavaScript 运行机制详解:再谈Event Loop》 是解开本次探讨答案的关键,建议仔细阅读

    一、为什么Javascript是单线程?

    JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
     
    所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
     
    二、任务队列
     
    单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

    JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备(很慢),挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

    所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
    - 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
    - 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
     
    具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
     
    1、所有同步任务都在主线程上执行,形成一个执行栈
    2、主线程之外,还存在一个 “任务队列”。只要异步任务有了运行结果,就在 “任务队列” 中,放置一个事件
    3、一旦 “执行栈” 中的所有同步任务执行完毕,系统就会读取 “任务队列”,看看里面有哪些事件,于是那些与事件相对应的异步任务结束等待状态,进入执行栈,开始执行
    4、主线程不断重复第三步操作
     
    只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复
     
    三、事件和回调函数
     
    前面提到过,“任务队列” 其实是一个事件的队列,当IO设备完成一项任务时,就在 “任务队列” 中添加一个事件,主线程读取 “任务队列”,就是读取里面有哪些事件
     
    “任务队列” 中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等)。只要指定过回调函数,这些事件发生时就会进入 “任务队列”,等待主线程读取
     
    而所谓 “回调函数”,就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,其实就是执行对应的回调函数
     
    四、事件循环
     
    基于前面的分析,总结一下 “任务队列” 的特点:
     
    1、“任务队列” 是一个先进先出的数据结构,排在前面的事件,优先被主线程读取
    2、只要执行栈一清空,最早进入 “任务队列” 的事件会率先进入主线程
    3、如果 “任务队列” 中存在定时器,主线程会先检查一下执行时间,某些事件只有到了规定的时间,才能进入主线程
     
    主线程从 “任务队列” 中读取事件,这个过程是循环不断的,所以这种运行机制又称为事件循环(Event Loop)
     
    五、定时器
     
    “任务队列” 中除了放置异步任务的事件,还可以放置定时事件,即指定某些事件在多少事件后执行
     
    以 setTimeout(fn, delay) 为例,它接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数
      console.log(1);
      setTimeout(function(){console.log(2);},1000);
      console.log(3);
    
      // 1 3 2
    上面的代码输出结果毫无悬念,因为 setTimeout() 将第二行代码推迟到1秒钟以后才执行,但是,将延迟时间设为0以后依然输出同样的结果。理论上延迟时间为0表示的是不延迟、立即执行
     
    但是基于前面的介绍,JS 引擎在执行这段代码时,首先把第一行和第三行代码存入执行栈,把第二行代码存入 “任务队列”,只有当执行栈清空以后,主线程才会读取 “任务队列”,这里的 0毫秒实际上表示的意思是:执行栈清空以后,主线程立即读取存放在 “任务队列” 中的该段代码,所以输入的结果是 1 3 2
      console.log(1);
      setTimeout(function(){console.log(2);}, 0);
      console.log(3);
    
      // 1 3 2

    六、宏观任务(MacroTask)和 微观任务(MicroTask)

    在重学前端系列文章中,winter老师也引入了 “宏观任务” 和 “微观任务” 的概念
     
    - 宏观任务:宿主(我们)发起的任务
    - 微观任务:Javascript引擎发起的任务
     
    微观任务执行顺序始终先于宏观任务,并且每个宏观任务可以包含多个微观任务
     
    (此处纯属个人理解:宏观任务保存在 “任务队列” 中,微观任务保存在 执行栈中,事件循环其实也就是不断执行宏观任务)
     
      var r = new Promise(function(resolve, reject){
        console.log("a");
        resolve()
      });
      setTimeout(()=>console.log("d"), 0)
      r.then(() => console.log("c"));
      console.log("b")
    再回头来看看开头的一段代码,会不会豁然开朗了呢。JS 引擎首先会把Promise对象 和 console.log("b") 两个微观任务存入执行栈,把 setTimeout(宏观任务)存入 “任务队列”
    所以在输出 a 和 b 以后并不会按照预期那样立即从 “任务队列” 中读取 setTimeout,因为 then方法是微观任务Promise对象的回调函数,先于 setTimeout 执行
     
    如果对以上内容都没问题的话,可以再看一段示例代码
      Promise.resolve().then(()=>{
        console.log('1')
        setTimeout(()=>{
          console.log('2')
        },0)
      })
    
      setTimeout(()=>{
        console.log('3')
        Promise.resolve().then(()=>{
          console.log('4')
        })
      },0)
    在交流群中看到有的小伙伴还是不太清楚正确的执行顺序,基于前面的介绍,大致的分析过程及草图如下:
     
    1(红色):JS 引擎会把微观任务Promise存入执行栈,把宏观任务setTimeout存入 “任务队列”
    2(绿色):主线程率先运行执行栈中的代码,依次输入1,然后把绿框的setTimeout存入 “任务队列”
    3(蓝色):执行栈清空以后,会率先读取 “任务队列” 中最早存入的setTimeout(红框的那个),并把这个定时器存入栈中,开始执行。这个定时器中的代码都是微观任务,所以可以一次性执行,依次输出3 和 4
    4(紫色):重复第3步的操作,读取 “任务队列” 中最后存入的setTimeout(绿框的那个),输出2
     
    所以最终的输出结果就是 1 3 4 2
    如果把上面代码中的第二个 setTimeout 延迟时间从0改为3000,结果会稍有不同,按照上面的分析步骤来拆解应该也挺简单
      Promise.resolve().then(()=>{
        console.log('1')
        setTimeout(()=>{
          console.log('2')
        },0)
      })
    
      setTimeout(()=>{
        console.log('3')
        Promise.resolve().then(()=>{
          console.log('4')
        })
      }, 3000)
    
      // 1 2 3 4
    还有一段在知乎上挺热闹的代码,有人不解为什么不是输出 1 2 3 4 5,其实按照上面的分析步骤就完全可以解释这个问题
      setTimeout(function(){console.log(4)},0); 
      
      new Promise(function(resolve){ 
        console.log(1) 
        for( var i=0 ; i<10000 ; i++ ){
           i==9999 && resolve() 
        } 
        console.log(2) 
      }).then(function(){ 
        console.log(5) 
      }); 
      console.log(3);
    
      // 1  2  3  5  4 
    另外一个会让人感到迷惑的地方就是 resolve回调函数内部的那几行代码,输出1以后接着跑1000次循环才调用resolve方法,其实resolve()的意思是把 Promise对象实例的状态从pending变成 fulfilled(即成功)
    成功的回调就是对应的then方法。所以resolve() 后面的 console.log(2) 会先执行,因为 resolve() 回调函数是在本轮事件循环的末尾执行 (关于这部分内容,可以参考  Promise对象 一文)
     
    同理,如果把代码中的 resolve() 去掉,也就是说 Promise 实例的状态一直保持在pending,就永远不会输出5了
      setTimeout(function(){console.log(4)},0); 
      
      new Promise(function(resolve){ 
        console.log(1) 
        for( var i=0 ; i<10000 ; i++ ){
          //  i==9999 && resolve() 
        } 
        console.log(2) 
      }).then(function(){ 
        console.log(5) 
      }); 
      console.log(3);
    
      // 1  2  3  4 
     
     
  • 相关阅读:
    8行代码批量下载GitHub上的图片
    python经典面试算法题1.1:如何实现链表的逆序
    pandas处理excel的常用方法技巧(上)
    numpy---python数据分析
    python、C++经典算法题:打印100以内的素数
    Java中数组、集合、链表、队列的数据结构和优缺点和他们之间的区别
    Hystrix
    Java中的static关键字解析
    Spring Boot
    Springcloud和Dubbo的区别。Eureka和Ribbon和Hystrix和zuul
  • 原文地址:https://www.cnblogs.com/rogerwu/p/10757069.html
Copyright © 2011-2022 走看看