vlambda博客
学习文章列表

【每日一题】(23题)面试官问:详细描述事件循环Event Loop?

关注「松宝写代码」,精选好文,每日一题

2020,实「鼠」不易

2021,「牛」转乾坤

风劲潮涌当扬帆,任重道远须奋蹄!

一、前言

2020.12.23 立的 flag,每日一题,题目类型不限制,涉及到JavaScript,Node,Vue,React,浏览器,http等领域。

本文是:【每日一题】(23题)面试官问:详细描述事件循环Event Loop?

这个时候并没有说是node的事件循环还是浏览器的事件循环,这个需要跟面试官确认一下,大多数是需要回答顺序:

  • 浏览器事件循环

  • Node事件循环

  • 一道输出值面试题

二、浏览器的事件循环

1、宏任务(Macro-Task)队列和微任务(Micro-Task)队列

浏览器事件循环中的异步队列有两种:macro(宏任务)队列和micro(微任务)队列。宏任务队列可以是多个,微任务队列只有一个。

  • 常见的macro-task:script代码块,setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O操作,UI rendering渲染页面等。

  • 常见的micro-task:process.nextTick,MutationObserver监听(h5新特性),Promise.then,async/await,ajax,axios,catch finally,Object.observe(方法废弃)等。

2、详细过程:

  • 1、浏览器按照js的顺序加载script标签分隔的代码块。

  • 2、script代码块加载完毕,首先进行语法分析,一旦语法错误,就会跳出当前的script代码块。

  • 3、语法分析正确之后,立即进行预编译阶段。

  • 4、预编译阶段:1、创建变量对象(创建arguments对象,函数声明提前,变量声明提升);2、确定作用域链;3、this指向。

  • 5、然后进入执行阶段。

  • 6、当前执行栈为空,执行栈是一个函数调用的栈结构,先进后出的原则。微任务队列为空。宏任务队列只有一个script代码块。

  • 7、全局上下文被推入执行栈,同步代码执行。执行过程中,会判断同步还是异步,通过一些接口调用和定时器,I/O等,产生新的宏任务和微任务,然后分别推进各自的任务队列中。同步代码执行完了,script代码块会被移出宏任务队列。这个过程就是队列的宏任务的执行和出队列过程。

  • 8、上一个出队是一个宏任务,这一步我们开始处理微任务。注意点:宏任务出队时,任务是一个一个执行,而微任务出队,任务是一队一队的执行。因此,我们开始处理微任务队列,会逐个执行队列中的任务,知道微任务队列被清空。

  • 9、执行渲染操作,更新页面。

  • 10、检查是否有web worker任务,如果有,对其处理。

  • 11、重复执行6--10过程,直到两个队列都被清空。

  • 12、重复执行1--11过程,直到所有代码块执行完毕。

三、Node的事件循环

1、简介

Node的事件循环和浏览器的事件循环是完全不同的东西。Node是采用v8作为js的解析引擎,而I/O处理也是使用自己设计的libuv库。libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统的一些底层特性,对外提供统一的接口API,事件循环机制也是libuv里面的实现。

【每日一题】(23题)面试官问:详细描述事件循环Event Loop?

Node的运行机制:

  • V8引擎解析JavaScript脚本。

  • 解析代码后,调用Node API。

  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务结果返回给V8引擎。

  • V8引擎再将结果返回给用户。

2、过程六个阶段

事件循环分为6个阶段,会按照顺序反复运行。每进入到某一个阶段,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量达到系统设定的阈值,就会进入到下一阶段。

【每日一题】(23题)面试官问:详细描述事件循环Event Loop?

(1)node事件循环的顺序:

  • 外部输入数据 --》轮询阶段(poll)--》检查阶段(check)--》关闭事件回调阶段(close callback)--》定时器检测阶段(timers)--》I/O事件回调阶段(I/O callbacks)--》闲置阶段(idle,prepare)--》轮询阶段(按照顺序反复运行)

说明:

  • timers阶段:这个阶段执行timer(setTimeout,setInterval等)回调。

  • I/O callbacks阶段:处理一些上一轮循环中的少数未执行的I/O回调。

  • idle,prepare阶段:仅node内部使用。

  • poll阶段:获取新的I/O事件,适当条件下node将阻塞在这里。

  • check阶段:执行setImmediate()的回调。

  • close callbacks阶段:执行socket的close事件回调。

注意:6个阶段不包括process.nextTick()

日常开发中,主要是timers阶段,poll阶段,check阶段包含了绝大部分异步任务。

(2)定时器检测阶段-timers阶段

timers阶段会执行setTimeout和setInterval回调,并且由轮询poll阶段控制。同样,在Node中定时器指定的时间也不是准确时间,只能尽快执行。

(3)轮询阶段--poll阶段

轮询poll阶段是一个重要阶段,主要做两件事情:

  • 1、回到timers阶段执行回调

  • 2、执行I/O回调

进入到这个阶段如果没有设定timer的话,会做个判断:

  • 1、如果poll队列不为空,会遍历回调队列并同步执行,知道队列为空或者达到系统限制。

  • 2、如果队列为空,会做个判断:

    • 如果有setImmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调。

    • 如果没有setImmediate回调,会等待回调被加入到队列中并立即执行回调,这里会有个超时时间设置防止一直等待下去

当然设定了timer,并且poll队列为空,会判断是否有timer超时,如果有的话回到timer阶段执行回调。

(4)检查阶段--check阶段

setImmediate的回调会被加到check队列中,从EventLoop阶段图知道,check阶段的执行顺序是在poll阶段之后。

我们看个栗子,更容易让我们理解:

 
   
   
 
  1. console.log('start')

  2. setTimeout(() => {

  3. console.log('timer1')

  4. Promise.resolve().then(function() {

  5. console.log('promise1')

  6. })

  7. }, 0)

  8. setTimeout(() => {

  9. console.log('timer2')

  10. Promise.resolve().then(function() {

  11. console.log('promise2')

  12. })

  13. }, 0)

  14. Promise.resolve().then(function() {

  15. console.log('promise3')

  16. })

  17. console.log('end')

  18. // start,

  19. // end,

  20. // promise3,

  21. // timer1,

  22. // timer2,

  23. // promise1,

  24. // promise2

分析:

  • 1、一开始执行栈的同步任务(宏任务),执行完毕后,依次打出 start和end,并将2个timer依次放入timers队列。

  • 2、然后去执行微任务(和浏览器有点像),打印出promise3。

  • 3、然后进入到timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入微任务队列,同样的步骤执行timer2,打印timer2。(这个和浏览器差别最大的地方),timers阶段有几个setTimeout/setInterval都会依次执行,并不像浏览器端,没执行一个宏任务后就去执行一个微任务队列。

3、node的微任务和宏任务

Node端事件循环中的异步队列也是这两种:macro(宏任务)队列和 micro(微任务)队列。

  • 1、常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。

  • 2、常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等。

4、注意点

(1)setTimeout和setImmediate

二者比较相似,区别:调用的时机不同。

  • 1、setImmediate:设计在poll阶段完成时执行,也就是在check阶段执行。

  • 2、setTimeout:设计在poll阶段空闲的时候,设定的时间达到后执行,也就是在timer阶段执行。

 
   
   
 
  1. setTimeout(function timeout () {

  2. console.log('timeout');

  3. },0);

  4. setImmediate(function immediate () {

  5. console.log('immediate');

  6. });

分析上述代码:

  • 执行之后,发现:setTimeout可能执行在前,也有可能执行在后。

  • 源码中,setTimeout(fn, 0) === setTimeout(fn, 1),进入事件循环也是需要成本的,如果在准备时候花费大于1ms,timer阶段就会直接执行setTimeout回调。

  • 如果准备时间花费小于1ms,那么就setImmediate回调先执行。

但是如果两者在异步I/O callback内部调用,总是先执行setImmediate,再执行setTimeout。

 
   
   
 
  1. const fs = require('fs')

  2. fs.readFile(__filename, () => {

  3. setTimeout(() => {

  4. console.log('timeout');

  5. }, 0)

  6. setImmediate(() => {

  7. console.log('immediate');

  8. })

  9. })

  10. // immediate

  11. // timeout

(2)process.nextTick

process.nextTick这个函数其实是独立于Event Loop之外的,有自己的队列。当每个阶段完成后,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。

 
   
   
 
  1. Promise.resolve().then(function() {

  2. console.log('promise0')

  3. })


  4. setTimeout(() => {

  5. console.log('timer1')

  6. Promise.resolve().then(function() {

  7. console.log('promise1')

  8. })

  9. }, 100)


  10. process.nextTick(() => {

  11. console.log('nextTick')

  12. process.nextTick(() => {

  13. console.log('nextTick')

  14. process.nextTick(() => {

  15. console.log('nextTick')

  16. process.nextTick(() => {

  17. console.log('nextTick')

  18. })

  19. })

  20. })

  21. })


  22. setTimeout(() => {

  23. console.log('timer2')

  24. Promise.resolve().then(function() {

  25. console.log('promise2')

  26. })

  27. }, 1)


  28. Promise.resolve().then(function() {

  29. console.log('promise3')

  30. })



  31. // nextTick

  32. // nextTick

  33. // nextTick

  34. // nextTick

  35. // promise0

  36. // promise3

  37. // timer2

  38. // promise2

  39. // timer1

  40. // promise1

Node的事件循环与浏览器差异

  • 浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。

  • 浏览器环境中,微任务的任务队列是在每一个宏任务执行完成之后执行。node中,微任务会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行微任务队列的任务。

【每日一题】(23题)面试官问:详细描述事件循环Event Loop?

我们看一个栗子,来说明两者区别:

 
   
   
 
  1. setTimeout(()=>{

  2. console.log('timer1')

  3. Promise.resolve().then(function() {

  4. console.log('promise1')

  5. })

  6. }, 0)

  7. setTimeout(()=>{

  8. console.log('timer2')

  9. Promise.resolve().then(function() {

  10. console.log('promise2')

  11. })

  12. }, 0)

1、浏览器端运行情况

输出:

 
   
   
 
  1. // timer1

  2. // promise1

  3. // timer2

  4. // promise2

浏览器端处理过程

图片上传失败,可以查看原文

2、Node端运行情况

node端运行需要分为两种情况:

  • 如果node11版本及之后,一旦执行一个阶段里的宏任务(setTimeout,setInterval,setImmediate)就会立刻执行微任务队列,跟浏览器端运行一致。最后结果:

 
   
   
 
  1. // timer1

  2. // promise1

  3. // timer2

  4. // promise2

  • 如果是node10及之前版本,要看第一个定时器执行完,第二定时器是否在完成队列中。

    • 如果第二个计时器未在完成队列中,结果为: js// timer1 // promise1 // timer2 // promise2

    • 如果第二个计时器已经在完成队列中,结果为: js// timer1 // timer2 // promise1 // promise2

我们来分析一下第二个计时器不在任务队列中的情况:

1、全局脚本main执行,将2个timer依次放入timer队列,main执行完后,调用栈为空闲,任务队列开始执行。

2、首先会进入timers阶段,执行timer1的回调函数,打印timer1,并将promsie1.then回调放入微任务队列。同样的步骤执行timer2,打印timer2。

3、至此,timer阶段执行结束,EventLoop进入下一阶段之前,执行微任务队列的所有任务,依次打印promise1,promise2。

node端处理过程:

五、一道输出值面试题

看个栗子

浏览器端和node端执行输出:

 
   
   
 
  1. console.log('1');


  2. setTimeout(function() {

  3. console.log('2');

  4. process.nextTick(function() {

  5. console.log('3');

  6. })

  7. new Promise(function(resolve) {

  8. console.log('4');

  9. resolve();

  10. }).then(function() {

  11. console.log('5')

  12. })

  13. })

  14. process.nextTick(function() {

  15. console.log('6');

  16. })

  17. new Promise(function(resolve) {

  18. console.log('7');

  19. resolve();

  20. }).then(function() {

  21. console.log('8')

  22. })


  23. setTimeout(function() {

  24. console.log('9');

  25. process.nextTick(function() {

  26. console.log('10');

  27. })

  28. new Promise(function(resolve) {

  29. console.log('11');

  30. resolve();

  31. }).then(function() {

  32. console.log('12')

  33. })

  34. })

输出:1,7,6,8,2,4,3,5,9,11,10,12

一共三轮事件循环:

第一轮:

宏任务:输出1;new Promise同步任务,输出7。

微任务队列中:process.nextTick属于微任务,输出6;然后输出8。

第一轮结束,输出1,7,6,8

第二轮:

宏任务:输出2;new Promise同步任务,输出4。

微任务队列中:process.nextTick属于微任务,输出3;然后输出5。

第三轮:

宏任务:输出9;new Promise同步任务,输出11。

微任务队列中:process.nextTick属于微任务,输出10;然后输出12。


Reference

1、https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick

2、https://developer.ibm.com/zh/languages/node-js/tutorials/learn-nodejs-the-event-loop/

3、http://lynnelv.github.io/js-event-loop-browser

谢谢支持

1、文章喜欢的话可以「分享,点赞,在看」三连哦。

3、长按下面图片,关注「松宝写代码」,是获取开发知识体系构建,精选文章,项目实战,实验室,每日一道面试题,进阶学习,思考职业发展,涉及到JavaScript,Node,Vue,React,浏览器,http等领域,希望可以帮助到你,我们一起成长~

字节内推福利

  • 回复「校招」获取内推码

  • 回复「社招」获取内推

  • 回复「实习生」获取内推

后续会有更多福利

学习资料福利

回复「算法」获取算法学习资料

往期「每日一题」

1、JavaScript && ES6

  • 第 22 题:

  • 第 21 题:

  • 第 20 题:

  • 第 19 题:

  • 第 18 题:

  • 第 17 题:

  • 第 16 题:

  • 第 15 题:

  • 第 14 题:

  • 第 13 题

  • 第 12 题

  • 第 11 题

  • 第 10 题

  • 第 8 题

  • 第 7 题

  • 第 6 题

  • 第 3 道

  • 第 2 道

2、浏览器

  • 第 9 题

3、Vue

  • 第 5 道

4、算法

  • 第 4 道

5、Http

  • 第 1 道