原文:最后一次搞懂 Event Loop

  自打 ES 6 推出 Promise 改善了 js 的异步编程之后,eventloop 也越来越多地出现在视野当中。借用大佬们的话:“Event Loop 是 JavaScript 异步编程的核心思想,也是前端进阶必须跨越的一关。同时,它又是面试的必考点。” 话不多说,上代码。

镇楼题:

async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
} async function async2() {
console.log('async2');
} console.log('script start'); setTimeout(function() {
console.log('setTimeout');
}, 0); async1(); new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}); console.log('script end');

执行栈和任务队列

内存中的数据结构:

  • 栈 (stack): 栈是遵循 后进先出 (LIFO) 原则的有序集合,新添加或待删除的元素都保存在同一端,称为栈顶,另一端叫做栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。栈在编程语言的编译器和内存中存储基本数据类型和对象的指针、方法调用等。
  • 队列 (queue): 队列是遵循 先进先出 (FIFO) 原则的有序集合,队列在尾部添加新元素,并在顶部移除元素,最新添加的元素必须排在队列的末尾。在计算机科学中,最常见的例子就是打印队列。
  • 堆 (heap): 堆是基于树抽象数据类型的一种特殊的数据结构。

执行栈

  在 js 中,当我们调用一个方法时,js 会生成执行上下文,这个执行上下文保存着该方法的私有作用域、上层作用域(作用域链)、方法参数、以及这个作用域中定义的变量和 this 的指向。

  当一系列的方法被调用的时候,由于 js 是单线程的,这些方法就会按照顺序被排列在一个单独的地方,这个地方就是执行栈。

事件队列

  事件队列是一个存储 异步任务 的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果事件队列不为空的话,事件队列便将第一个任务压入执行栈中运行。

 

事件循环 eventloop

  • 所有同步任务都在主线程上执行,形成一个执行栈 (Execution Context Stack)。
  • 而异步任务会被放置到 Task Table,当异步任务有了运行结果,就将该函数移入任务队列。
  • 一旦执行栈中的所有同步任务执行完毕,引擎就会读取任务队列,然后将任务队列中的第一个任务压入执行栈中运行。

主线程不断重复第三步,也就是 只要主线程空了,就会去读取任务队列 ,该过程不断重复,这就是所谓的 事件循环。

宏任务 和 微任务

一个银行案例:

  以去银行办业务为例,当 5 号窗口柜员处理完当前客户后,开始叫号来接待下一位客户,我们将每个客户比作 宏任务,接待下一个客户 的过程也就是让下一个 宏任务 进入到执行栈。

  所以该窗口所有的客户都被放入了一个 任务队列 中。任务队列中的都是 异步操作有了结果的,而不是注册一个异步任务就会被放在这个任务队列中(它会被放到 Task Table 中)。就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号。

  在执行宏任务时,是可以穿插一些微任务进去。比如你大爷在办完业务之后,顺便问了下柜员:“最近 P2P 暴雷很严重啊,有没有其他稳妥的投资方式”。柜员就会给出相应的回答。

  我们分析一下这个过程,虽然大爷已经办完正常的业务,但又咨询了一下理财信息,这时候柜员肯定不能说:“您再上后边取个号去,重新排队”。所以只要是柜员能够处理的,都会在响应下一个宏任务之前来做,我们可以把这些任务理解成是 微任务。

  大爷听罢,扬起 45 度微笑,说:“我就问问。”

  柜员 OS:“艹...”

  这个例子就说明了:你大爷永远是你大爷。 在当前微任务没有执行完成时,是不会执行下一个宏任务的!

  总结一下,异步任务分为 宏任务(macrotask) 与 微任务(microtask) 。宏任务会进入一个队列,而微任务会进入到另一个不同的队列,且微任务要优于宏任务执行。

常见的 宏任务 和 微任务

宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)

微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)

简单案例和分析:

1.

setTimeout(() => {
console.log('A');
}, 0); var obj = {
func: function() {
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
}; obj.func().then(function() {
console.log('D');
}); console.log('E');
  • 第一个 setTimeout 放到宏任务队列,此时宏任务队列为 ['A']
  • 接着执行 obj 的 func 方法,将 setTimeout 放到宏任务队列,此时宏任务队列为 ['A', 'B']
  • 函数返回一个 Promise,因为这是一个同步操作,所以先打印出 'C'
  • 接着将 then 放到微任务队列,此时微任务队列为 ['D']
  • 接着执行同步任务 console.log('E');,打印出 'E'
  • 因为微任务优先执行,所以先输出 'D'
  • 最后依次输出 'A''B'

要注意的是 obj.func().then() 这里,obj.func() 是普通函数/同步代码,后面的 then 才是微任务。

2.

function go() {
console.log(5)
} let p = new Promise(resolve => {
resolve(1);
Promise.resolve(go()).then(() => console.log(2));
console.log(4);
}).then(t => console.log(t)); console.log(3);
  • 首先执行同步代码 Promise.resolve(go()) 中 go(),打印 5
  • 首先将 Promise.resolve() 的 then() 方法放到微任务队列,此时微任务队列为 ['2']
  • 然后打印出同步任务 4
  • 接着将 p 的 then() 方法放到微任务队列,此时微任务队列为 ['2', '1']
  • 打印出同步任务 3
  • 最后依次打印微任务 21

要注意的是 Promise.resolve(go()) 这里也是普通函数/同步代码,当执行到这一行的时候,会立即执行 go(),后面的 then 才是微任务。

至此,与基础 Promise 相关的 eventloop 执行过程分析完毕。

当 Event Loop 遇到 async/await

  在 es 7 中引入了 async/await 的语法,当他们会与 eventloop 发生什么样的反应呢?

  根据定义,我们知道,async/await 仅仅是生成器的语法糖,所以不要怕,只要把它转换成 Promise 的形式即可。下面这段代码是 async/await 函数的经典形式。

async function foo() {
// await 前面的代码
await bar();
// await 后面的代码
} async function bar() {
// do something...
} foo();
  其中 await 前面的代码 是同步的,调用此函数时会直接执行;而 await bar(); 这句可以被转换成 Promise.resolve(bar())await 后面的代码 则会被放到 Promise 的 then() 方法里。

  改写如下:

function foo() {
// await 前面的代码
Promise.resolve(bar()).then(() => {
// await 后面的代码
});
} function bar() {
// do something...
} foo();

所以,开篇的那条镇楼题可以改写成这样:

function async1() {
console.log('async1 start'); // 2 Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
} function async2() {
console.log('async2'); // 3
} console.log('script start'); // 1 setTimeout(function() {
console.log('settimeout'); // 8
}, 0); async1(); new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
console.log('script end'); // 5
  • 首先打印出 script start
  • 接着将 settimeout 添加到宏任务队列,此时宏任务队列为 ['settimeout']
  • 然后执行函数 async1,先打印出 async1 start,又因为 Promise.resolve(async2()) 是同步任务,所以打印出 async2,接着将 async1 end 添加到微任务队列,,此时微任务队列为 ['async1 end']
  • 接着打印出 promise1,将 promise2 添加到微任务队列,,此时微任务队列为 ['async1 end', promise2]
  • 打印出 script end
  • 因为微任务优先级高于宏任务,所以先依次打印出 async1 endpromise2
  • 最后打印出宏任务 settimeout

关于这道题的争议:大多都是 async1 endpromise2 的顺序问题。在 Chrome 73.0.3683.103 for MACNode.js v8.15.1 测试是 async1 end 先于 promise2,在 FireFox 66.0.3 for MAC 测试是 async1 end 后于 promise2

另外:
关于最后的结果顺序,国外的一篇文章,写的很详细,并且有详细的测试。 结果就是: 在不同的浏览器下,甚至是同一种浏览器的不同版本中,异步任务的执行顺序都会有差异,也就是说他们的优先级并不是完全固定的。注意,只是异步任务的优先级会有所不同,这主要还是各个浏览器的问题。

Event-Loop In JS的更多相关文章

  1. Node.js event loop 和 JS 浏览器环境下的事件循环的区别

    Node.js  event loop 和 JS 浏览器环境下的事件循环的区别: 1.线程与进程: JS 是单线程执行的,指的是一个进程里只有一个主线程,那到底什么是线程?什么是进程? 进程是 CPU ...

  2. 从Event Loop谈JS的运行机制

    这里主要是结合Event Loop来谈JS代码是如何运行的. 事件循环对于我们平时开发可以说是特别重要,可以让我们写出更好的代码. 到这里相信我们已经知道了JS引擎是单线程,而且这里会用到前面说的的几 ...

  3. Js 运行机制和Event Loop

    一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. Java ...

  4. JS event loop

    一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. Java ...

  5. 理解js事件循环(event loop)

    队列:先进先出 栈:后进先出 javascript的Event Loop 和 Node.js的Event Loop 区别: js(运行在浏览器),有主线程.异步任务队列的概念: node.js使用li ...

  6. 为什么JS是单线程?JS中的Event Loop(事件循环)?JS如何实现异步?setimeout?

    https://segmentfault.com/a/1190000012806637 https://www.jianshu.com/p/93d756db8c81 首先,请牢记2点: (1) JS是 ...

  7. The Node.js Event Loop, Timers, and process.nextTick() Node.js事件循环,定时器和process.nextTick()

    个人翻译 原文:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ The Node.js Event Loop, Ti ...

  8. The Node.js Event Loop, Timers, and process.nextTick()

    The Node.js Event Loop, Timers, and process.nextTick() | Node.js https://nodejs.org/uk/docs/guides/e ...

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

    原文地址:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 一年前,我写了一篇<什么是 Event Loop?>,谈了我对Eve ...

  10. JavaScript 运行机制详解:深入理解Event Loop

    Philip Roberts的演讲<Help, I'm stuck in an event-loop>,详细.完整.正确地描述JavaScript引擎的内部运行机制. 一.为什么JavaS ...

随机推荐

  1. Ocelot-基于.NET Core的开源网关实现

    写在前面 API网关是系统内部服务暴露在外部的一个访问入口,类似于代理服务器,就像一个公司的门卫承担着寻址.限制进入.安全检查.位置引导等工作,我们可以形象的用下图来表示: 外部设备需要访问内部系统服 ...

  2. 补习系列(12)-springboot 与邮件发送

    目录 一.邮件协议 关于数据传输 二.SpringBoot 与邮件 A. 添加依赖 B. 配置文件 C. 发送文本邮件 D.发送附件 E. 发送Html邮件 三.CID与图片 参考文档 一.邮件协议 ...

  3. MySQL性能基准测试对比:5.7 VS 8.0

    本文由云+社区发表 作者:数据库 版权声明:本文由腾讯云数据库产品团队整理,页面原始内容来自于severalnines英文官网,若转载请注明出处.翻译目的在于传递更多全球最新数据库领域相关信息,并不意 ...

  4. Spring Cloud Alibaba到底坑不坑?

    之前我发过一篇<说说我为什么看好Spring Cloud Alibaba>,然后这两天有网友给我转了这篇文章<坑爹项目spring-cloud-alibaba,我们也来一个>, ...

  5. 一文搞定MySQL的事务和隔离级别

    一.事务简介 事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成. 一个数据库事务通常包含了一个序列的对数据库的读/写操作.它的存在包含有以下两个目的: 为数据库操作序列提供 ...

  6. 数据结构——Java实现顺序表

    一.分析 什么是顺序表?顺序表是指用一组地址连续的存储单元依次存储各个元素,使得在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中的线性表.一个标准的顺序表需要实现以下基本操作: 1.初始化顺序表 ...

  7. iOS视频边下载边播放

    随着视频行业的发展,很多用户对于观看体验也有了更高的要求,以前的习惯是下载好了在观看,而现在是希望1分钟都不要等,ZUI好一边看着一边下载,等把这个视频看完也下载完了,也就是我们常说的“视频边下载边播 ...

  8. 使用 Scrapy 爬取股票代码

    个人博客: https://mypython.me 源码地址: https://github.com/geeeeeeeek/scrapy_stock 抓取工具:scrapy scrapy 介绍 Scr ...

  9. Android Studio教程09-加载器Loader的使用

    目录 1.加载器特征 2. Loader API 3. 在应用中使用Loader 3.1. 启动加载器 3.2. 重启加载器 3.3. 使用LoaderManager回调 4. 实例: 访问用户联系人 ...

  10. 业务与IT技术

    最近听一个同事又再次提问关于业务比技术重要,是真的吗? 今天我们再来看一下.      一,什么是业务? 业务意指某种有目的的工作或工作项目.技术可以指人类对机器.硬件或人造器皿的运用,但它也可以包含 ...