阮老师在其推特上放了一道题:

new Promise(resolve => {resolve(1);Promise.resolve().then(() => console.log(2));console.log(4)
}).then(t => console.log(t));
console.log(3);

看到此处的你可以先猜测下其答案,然后再在浏览器的控制台运行这段代码,看看运行结果是否和你的猜测一致。

事件循环

众所周知,JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。根据 HTML 规范:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

本文所涉及到的事件循环是基于 Browsing Context。

那么在事件循环机制中,又通过什么方式进行函数调用或者任务的调度呢?

任务队列

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)

  • 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue

  • 更新 render

  • 主线程重复执行上述步骤

仔细查阅规范可知,异步任务可分为 task 和 microtask 两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

查阅了网上比较多关于事件循环介绍的文章,均会提到 macrotask(宏任务) 和 microtask(微任务) 两个概念,但规范中并没有提到 macrotask,因而一个比较合理的解释是 task 即为其它文章中的 macrotask。另外在 ES2015 规范中称为 microtask 又被称为 Job。

(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)

microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

在 Node 中,会优先清空 next tick queue,即通过process.nextTick 注册的函数,再清空 other queue,常见的如Promise

setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

示例

纯文字表述确实有点干涩,这一节通过一个示例来逐步理解:

console.log('script start');setTimeout(function() {console.log('timeout1');
}, 10);new Promise(resolve => {console.log('promise1');resolve();setTimeout(() => console.log('timeout2'), 10);
}).then(function() {console.log('then1')
})console.log('script end');

首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:

然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:

script 任务继续往下执行,遇到 Promise 实例。Promise 构造函数中的第一个参数,是在 new 的时候执行,构造函数执行时,里面的参数进入执行栈执行;而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 then1 分配到对应队列。

构造函数继续往下执行,又碰到 setTimeout,然后将对应的任务分配到对应队列:

script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。

根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。

因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中,只有 Promise队列中的一个任务 then1,因此直接执行就行了,执行结果输出 then1。当所有的 microtast 执行完毕之后,表示第一轮的循环就结束了。

这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务 macrotask开始。此时,有两个宏任务:timeout1 和 timeout2

取出 timeout1 执行,输出 timeout1。此时微任务队列中已经没有可执行的任务了,直接开始第三轮循环:

第三轮循环依旧从宏任务队列开始。此时宏任务中只有一个 timeout2,取出直接输出即可。

这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。那么例子的输出结果就显而易见:

script start
promise1
script end
then1
timeout1
timeout2

总结

在回头看本文最初的题目:

new Promise(resolve => {resolve(1);Promise.resolve().then(() => {// t2console.log(2)});console.log(4)
}).then(t => {// t1console.log(t)
});
console.log(3);

这段代码的流程大致如下:

  1. script 任务先运行。首先遇到 Promise 实例,构造函数首先执行,所以首先输出了 4。此时 microtask 的任务有 t2 和 t1

  2. script 任务继续运行,输出 3。至此,第一个宏任务执行完成。

  3. 执行所有的微任务,先后取出 t2 和 t1,分别输出 2 和 1

  4. 代码执行完毕

综上,上述代码的输出是:4321

为什么 t2 会先执行呢?理由如下:

  • 根据 Promises/A+规范:

实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行

  • Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的 Promise 对象。立即 resolved 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

es6.ruanyifeng.com/#docs/promi…

所以,t2 比 t1 会先进入 microtask 的 Promise 队列。

相关链接

  • JavaScript 运行机制详解:再谈Event Loop

  • How JavaScript Timers Work 对应的译文

  • Event Loop的规范和实现

  • Promise-resolve

  • Promises/A+规范

原地址 : https://juejin.im/entry/5a8bc3215188257a856f4b2b

从一道题浅说 JavaScript 的事件循环相关推荐

  1. 深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

    深入理解JavaScript的事件循环(Event Loop) 一.什么是事件循环 JS的代码执行是基于一种事件循环的机制,之所以称作事件循环,MDN给出的解释为因为它经常被用于类似如下的方式来实现 ...

  2. 技术干货 | JavaScript 之事件循环(Event Loop)

    导读:学过 JavaScript(下文简称 JS) 的都知道它是一门单线程的.非阻塞的脚本语言.单线程意味着,JS 代码在执行的任何时候,都只有一个主线程来处理所有的任务,这也就意味着 JS 无法进行 ...

  3. 深入理解JavaScript的事件循环

    最近阅读<高性能JavaScript>时,第六章谈到"通过定时器将JavaScript执行代码的控制权先让给浏览器用于更新UI状态,然后再将控制权交回给JavaScript代码, ...

  4. 聊聊Javascript的事件循环

    JavaScript.浏览器.事件之间的关系 JavaScript程序采用了异步事件驱动编程(Event-driven programming)模型,维基百科对它的解释是: 事件驱动程序设计(英语:E ...

  5. JavaScript中事件循环的理解 Event Loop

    为了解决单线程运行阻塞问题,JavaScript用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop) 事件循环(Event Loop) 在JavaScript中,所有的任务都 ...

  6. JavaScript 执行— 事件循环、宏观任务、微观任务

    从浏览器或者 Node 开发者的角度来看,我们应该如何使用 JS 引擎? 当拿到一段 JS 代码时,浏览器或者 Node 环境首先要做的就是:传递给 JS 引擎,并要求它去执行.然而,执行 JS 并非 ...

  7. 原生js循环展示dom_【前端面试】用一道题讲 js 的事件循环队列

    昨天去面了滴滴,一口气面了三面,考了 promise 和事件循环.之前的猿辅导也考察了这些,几乎所有的大厂中厂都一定会考原生 js 的事件循环队列. 今天,我把昨天考察的原题拿出来分析一下. setT ...

  8. JavaScript事件循环探索

    一直对js的事件循环不是很清晰,最近看了JavaScript忍者秘籍的第13章后,有了一些感悟,特此总结一下,分享给大家. 单线程 众所周知,JavaScript是单线程执行模型,同一时刻只能执行一个 ...

  9. JAVA script 循环 图片_深入分析JavaScript 事件循环(Event Loop)

    事件循环(Event Loop),是每个JS开发者都会接触到的概念,但是刚接触时可能会存在各种疑惑. 众所周知,JS是单线程的,即同一时间只能运行一个任务.一般情况下这不会引发问题,但是如果我们有一个 ...

最新文章

  1. 第二十五章 面向对象------封装、内置函数、反射、动态导入
  2. LeetCode 5620.连续连接的二进制数字
  3. 程序员的小天地:注释中的快乐
  4. 试水区块链出版?纽约时报在招人了
  5. centos 6.5 下 nginx 简单优化_虚拟主机_负载均衡
  6. JS正则表达式验证数字非常全
  7. springboot约定优于配置的体现
  8. Python利用描述符进行属性访问控制,完成属性数据类型强制定义(如C语言)、属性读写及删除操作
  9. 二叉树遍历代码_二叉树的题,就那几个框架,枯燥至极
  10. yconsole使用说明
  11. 如何将qq客服搬到我们的flex应用中
  12. mysql workbench安装出错_MYSQL及MySQL WORKBENCH安装过程遇到的问题及处理方法
  13. POJ3981 字符串替换【水题】
  14. 基于W5500+Yeelink的远程灯光控制设计
  15. Android保活实现方案梳理
  16. 网站跳出率的相关要点介绍 1
  17. 偶然还是必然——读小蚂蚁的罗马人故事II
  18. 互联网+对酒店IPTV电视系统的影响
  19. hadoop运行程序时报错java.net.ConnectException: java.net.ConnectException: 拒绝连接;
  20. Linux中 安装一些实用小软件总结

热门文章

  1. 7个Debug linux程序的Strace 列子
  2. Squid代理服务器基本配置(三)
  3. xp/win7,添加开机启动项
  4. Excel事半功倍的应用
  5. HP服务器选型一般标准
  6. 检查当前ORACLE连接数
  7. vmware无法打开ubuntu解决办法
  8. mysql insert 数据_MySQL-插入数据(INSERT)
  9. android荧光进度条,CSS3 彩色荧光棒进度条
  10. Python中eval函数的表达式如何使用