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

  1. var r = new Promise(function(resolve, reject){
  2. console.log("a");
  3. resolve()
  4. });
  5. setTimeout(()=>console.log("d"), 0)
  6. r.then(() => console.log("c"));
  7. console.log("b")
  8.  
  9. // 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) 为例,它接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数
  1. console.log(1);
  2. setTimeout(function(){console.log(2);},1000);
  3. console.log(3);
  4.  
  5. // 1 3 2
上面的代码输出结果毫无悬念,因为 setTimeout() 将第二行代码推迟到1秒钟以后才执行,但是,将延迟时间设为0以后依然输出同样的结果。理论上延迟时间为0表示的是不延迟、立即执行
 
但是基于前面的介绍,JS 引擎在执行这段代码时,首先把第一行和第三行代码存入执行栈,把第二行代码存入 “任务队列”,只有当执行栈清空以后,主线程才会读取 “任务队列”,这里的 0毫秒实际上表示的意思是:执行栈清空以后,主线程立即读取存放在 “任务队列” 中的该段代码,所以输入的结果是 1 3 2
  1. console.log(1);
  2. setTimeout(function(){console.log(2);}, 0);
  3. console.log(3);
  4.  
  5. // 1 3 2

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

在重学前端系列文章中,winter老师也引入了 “宏观任务” 和 “微观任务” 的概念
 
- 宏观任务:宿主(我们)发起的任务
- 微观任务:Javascript引擎发起的任务
 
微观任务执行顺序始终先于宏观任务,并且每个宏观任务可以包含多个微观任务
 
(此处纯属个人理解:宏观任务保存在 “任务队列” 中,微观任务保存在 执行栈中,事件循环其实也就是不断执行宏观任务)
 
  1. var r = new Promise(function(resolve, reject){
  2. console.log("a");
  3. resolve()
  4. });
  5. setTimeout(()=>console.log("d"), 0)
  6. r.then(() => console.log("c"));
  7. console.log("b")
再回头来看看开头的一段代码,会不会豁然开朗了呢。JS 引擎首先会把Promise对象 和 console.log("b") 两个微观任务存入执行栈,把 setTimeout(宏观任务)存入 “任务队列”
所以在输出 a 和 b 以后并不会按照预期那样立即从 “任务队列” 中读取 setTimeout,因为 then方法是微观任务Promise对象的回调函数,先于 setTimeout 执行
 
如果对以上内容都没问题的话,可以再看一段示例代码
  1. Promise.resolve().then(()=>{
  2. console.log('1')
  3. setTimeout(()=>{
  4. console.log('2')
  5. },0)
  6. })
  7.  
  8. setTimeout(()=>{
  9. console.log('3')
  10. Promise.resolve().then(()=>{
  11. console.log('4')
  12. })
  13. },0)
在交流群中看到有的小伙伴还是不太清楚正确的执行顺序,基于前面的介绍,大致的分析过程及草图如下:
 
1(红色):JS 引擎会把微观任务Promise存入执行栈,把宏观任务setTimeout存入 “任务队列”
2(绿色):主线程率先运行执行栈中的代码,依次输入1,然后把绿框的setTimeout存入 “任务队列”
3(蓝色):执行栈清空以后,会率先读取 “任务队列” 中最早存入的setTimeout(红框的那个),并把这个定时器存入栈中,开始执行。这个定时器中的代码都是微观任务,所以可以一次性执行,依次输出3 和 4
4(紫色):重复第3步的操作,读取 “任务队列” 中最后存入的setTimeout(绿框的那个),输出2
 
所以最终的输出结果就是 1 3 4 2
如果把上面代码中的第二个 setTimeout 延迟时间从0改为3000,结果会稍有不同,按照上面的分析步骤来拆解应该也挺简单
  1. Promise.resolve().then(()=>{
  2. console.log('1')
  3. setTimeout(()=>{
  4. console.log('2')
  5. },0)
  6. })
  7.  
  8. setTimeout(()=>{
  9. console.log('3')
  10. Promise.resolve().then(()=>{
  11. console.log('4')
  12. })
  13. }, 3000)
  14.  
  15. // 1 2 3 4
还有一段在知乎上挺热闹的代码,有人不解为什么不是输出 1 2 3 4 5,其实按照上面的分析步骤就完全可以解释这个问题
  1. setTimeout(function(){console.log(4)},0);
  2.  
  3. new Promise(function(resolve){
  4. console.log(1)
  5. for( var i=0 ; i<10000 ; i++ ){
  6. i==9999 && resolve()
  7. }
  8. console.log(2)
  9. }).then(function(){
  10. console.log(5)
  11. });
  12. console.log(3);
  13.  
  14. // 1 2 3 5 4
另外一个会让人感到迷惑的地方就是 resolve回调函数内部的那几行代码,输出1以后接着跑1000次循环才调用resolve方法,其实resolve()的意思是把 Promise对象实例的状态从pending变成 fulfilled(即成功)
成功的回调就是对应的then方法。所以resolve() 后面的 console.log(2) 会先执行,因为 resolve() 回调函数是在本轮事件循环的末尾执行 (关于这部分内容,可以参考  Promise对象 一文)
 
同理,如果把代码中的 resolve() 去掉,也就是说 Promise 实例的状态一直保持在pending,就永远不会输出5了
  1. setTimeout(function(){console.log(4)},0);
  2.  
  3. new Promise(function(resolve){
  4. console.log(1)
  5. for( var i=0 ; i<10000 ; i++ ){
  6. // i==9999 && resolve()
  7. }
  8. console.log(2)
  9. }).then(function(){
  10. console.log(5)
  11. });
  12. console.log(3);
  13.  
  14. // 1 2 3 4
 
 

重学前端 --- Promise里的代码为什么比setTimeout先执行?的更多相关文章

  1. Promise里的代码为什么比setTimeout先执行

    当浏览器或者Node拿到一段代码时首先做的就是传递给JavaScript引擎,并且要求它去执行. 然而,执行 JavaScript 并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给 J ...

  2. 重学前端--js是面向对象还是基于对象?

    重学前端-面向对象 跟着winter老师一起,重新认识前端的知识框架 js面向对象或基于对象编程 以前感觉这两个在本质上没有什么区别,面向对象和基于对象都是对一个抽象的对象拥有一系列的行为和状态,本质 ...

  3. try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?

    答:会执行,在方法返回调用者前执行.

  4. 「后端小伙伴来学前端了」Vuex进阶操作,让你的代码更加高效(简称如何学会偷懒 【手动狗头】)

    学妹手机里的美照 前言 前一篇写了Vuex基本使用,用起来还稍稍有些繁琐,代码有很多 冗余的地方,这篇就带着大家用更简单的方式来使用Vuex(其实就是怎么更好的偷懒,用更少的代码来完之前的事情) 进入 ...

  5. css与javascript重难点,学前端,基础不好一切白费!

    JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果.通常JavaScript脚本是通过嵌入在HTML中来实现 ...

  6. 谷哥的小弟学前端(01)——HTML常用标签(1)

    探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架 ...

  7. 重学 Java 设计模式:实战外观模式「基于SpringBoot开发门面模式中间件,统一控制接口白名单场景」

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你感受到的容易,一定有人为你承担不容易 这句话更像是描述生活的,许许多多的磕磕绊绊总 ...

  8. 推翻自己和过往,重学自定义View

    http://blog.csdn.net/lfdfhl/article/details/51671038 深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 A ...

  9. 为何你跟着滴滴D8级前端大神撸代码,技术却依旧原地踏步?

    引子 听说最近有很多小伙伴,热衷于在慕课网上学习各种前端实战教程,并以完成项目为奋斗目标.比如本文接下来要提到的<Vue2.0高级实战之开发移动端音乐App>,这门课程的传授者是来自滴滴D ...

随机推荐

  1. 同一台电脑上配置多个解压版tomcat方法(本例安装两个)

    一.在环境变量中设置变量() CATALINA_HOME = tomcat路径一 CATALINA_BASE = tomcat路径一 CATALINA_HOME2 = tomcat路径二 CATALI ...

  2. 想要薪资20-30K,Python程序员认真敲代码就够了!

    在这个年代,互联网的飞速壮大大家有目共睹,除了表露出的公共受益,其中计算机编程者也是做出了巨大的贡献,即使外国编程语言仍旧属于领导者,但是在互联网的壮大下,我们这是"地球村",国内 ...

  3. 详解Trie

    一.Trie的概念 Trie又称字典树,前缀树(事实上前缀树这个名字就很好的解释了Trie的储存方式) 来一张图理解一下Trie的储存方式:(图片来自百度百科) 由这张图我们也可以知道Trie的特点: ...

  4. Coding theano under remote ubuntu server from local Mac (在本地mac机器上,写、跑、调试、看-远程ubuntu上的theano代码)

    本人是奇葩,最近鼓捣了一套在mac上coding远程ubuntu上的theano代码的东东,记之以期造福后人. Overview: 下图是我的编程环境和网络环境 我期望能在本地mac机器上对远程的ub ...

  5. PAT1099:Build A Binary Search Tree

    1099. Build A Binary Search Tree (30) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN ...

  6. PAT1064: Compelte Binary Search Tree

    1064. Complete Binary Search Tree (30) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHE ...

  7. SQL关于IN和EXISTS的用法和区别的比较

    1.exist,not exist一般都是与子查询一起使用. In可以与子查询一起使用,也可以直接in (a,b.....).2.exist会针对子查询的表使用索引. not exist会对主子查询都 ...

  8. IntelliJ IDEA 2018.1.2 安装及汉化教程(附:下载地址)

    附:安装包及汉化包下载地址  链接:https://pan.baidu.com/s/1ysxtVH_gnBm0QnnqB5mluQ 密码: 9pqd 1.安装步骤: 选择安装地址:可以默认.本人安装在 ...

  9. ORALCE删除临时表空间的方法---解决ORA01033: oralce initialization or shutdown in progress方案

    当一台主机上oralce 临时表空间太多,而又用不到这些临时表空间的时候,    TABLESPACE 会占用大量的存储空间.本文介绍一种删除ORACLE 临时表空间的方法. 一 启动任务管理器.在任 ...

  10. 详解vue生命周期

    vue生命周期 @(vue)[生命周期] 前言 在使用vue一个多礼拜后,感觉现在还停留在初级阶段,虽然知道怎么和后端做数据交互,但是对于mounted这个挂载还不是很清楚的.放大之,对vue的生命周 ...