十六. 事件循环_ 面试题

84 阅读6分钟

十六. 事件循环_ 面试题

16.1. 浏览器的事件循环

如果在执行JavaScript代码的过程中,异步操作如何执行的呢?

  • 中间我们插入了一个setTimeout的函数调用;

    • setTimeout()本身不是一个异步的操作,传入的回调函数才是异步的
  • 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行

image-20220321231243424

image-20220321231243424.png

16.2. 宏任务和微任务

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

  • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等

那么事件循环对于两个队列的优先级是怎么样的呢?

  • 1.main script中的代码优先执行(编写的顶层script代码);

  • 2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行

    • 也就是宏任务执行之前,必须保证微任务队列是空的;
    • 如果不为空,那么就优先执行微任务队列中的任务(回调);

16.3. Promise面试题

 setTimeout(function () {
   console.log("setTimeout1");
   new Promise(function (resolve) {
     resolve();
   }).then(function () {
     new Promise(function (resolve) {
       resolve();
     }).then(function () {
       console.log("then4");
     });
     console.log("then2");
   });
 });
 ​
 new Promise(function (resolve) {
   console.log("promise1");
   resolve();
 }).then(function () {
   console.log("then1");
 });
 ​
 setTimeout(function () {
   console.log("setTimeout2");
 }); 
 ​
 console.log(2);
 ​
 queueMicrotask(() => {
   console.log("queueMicrotask1")
 });
 ​
 new Promise(function (resolve) {
   resolve();
 }).then(function () {
   console.log("then3");
 });
 ​
 // promise1
 // 2
 // then1
 // queueMicrotask1
 // then3
 // setTimeout1
 // then2
 // then4
 // setTimeout2

16.4. promise async await面试题

 async function bar() {
     console.log('222');
     return new Promise((resolve) => {
         resolve()
     }) 
 }
 ​
 async function foo() {
     // await和generator一样,会先执行第一段代码,然后执行excutor(),
     // 然后把then后面的内容加入到微任务,即'333'加到微任务
     console.log('111');
     await bar()
     console.log('333');
 }
 foo()
 console.log('4444');
 // 111 222 444 333
 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')
 ​
 // script start
 // async1 start
 // async2
 // promise1
 // script end
 // async1 end
 // promise2
 // setTimeout

16.5. Promise面试题(困难);掘金上某厂面试题

 Promise.resolve().then(() => {
   console.log(0);
   // return Promise
   // 不是普通的值, 多加一次微任务
   // Promise.resolve(4), 多加一次微任务
   // 一共多加两次微任务
   return Promise.resolve(4)
 }).then((res) => {
   console.log(res)
 })
 ​
 Promise.resolve().then(() => {
   console.log(1);
 }).then(() => {
   console.log(2);
 }).then(() => {
   console.log(3);
 }).then(() => {
   console.log(5);
 }).then(() =>{ 
   console.log(6);
 })

演变一:return 普通值

 Promise.resolve().then(() => {
     console.log(0);
     // 直接return一个值 相当于resolve(4)
     return 4
 }).then((res) => {
     console.log(res)
 })
 ​
 Promise.resolve().then(() => {
     // 这里的回调函数执行时,会把clg(2)回调函数加入微任务
     console.log(1);
 }).then(() => {
     // clg(2)加入微任务后,不是马上添加clg(3)回调函数,而是去执行微任务,
     // 等clg(2)执行时,才会添加clg(3)的微任务
     console.log(2);
 }).then(() => {
     console.log(3);
 }).then(() => {
     console.log(5);
 }).then(() => {
     console.log(6);
 })
 // 打印顺序: 0142356

演变二:return thenable对象

按理是没有区别,但是结果和直接return 4却不一样,原生Promise在执行thenable对象时,往后面推迟了一个微任务

  • 注:不能用promise A+规范去套执行结果,ES6的Promise遵循A+,但也做了一些功能拓展

结果不一样的原因:

  • 如果不是return一个普通的值,比如thenable对象,then函数不是直接执行的,而是推到下一个微任务后面去执行

问:为什么要推迟到下一个微任务?

  • 答:then是一个函数,如果里面包含了大量的计算代码,然后才会确定Promise的状态,那么会阻塞后面的微任务clg(2)

问:那推迟到下一个微任务,不照样会阻塞后面的clg(3)微任务吗?

  • 答:我们这里只是写了个特殊的案例去讨论执行流程,一般真实开发不会嵌套这么多微任务,有可能clg(2)执行完就没有微任务了,

大概是出于这样的一个考虑,做了一些优化,所以把then函数往后推了一个微任务

 Promise.resolve().then(() => {
     console.log(0); 
     return {
         then: function () {
             //函数内有大量的计算代码
             resolve(4)
         }
     }
 }).then((res) => {
     console.log(res)
 })
 ​
 Promise.resolve().then(() => {
     console.log(1);
 }).then(() => {
     console.log(2);
 }).then(() => {
     console.log(3);
 }).then(() => {
     console.log(5);
 }).then(() => {
     console.log(6);
 })
 // 打印顺序: 0124356

16.6. node的事件循环

浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:

  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
  • 早期js是应用在浏览器的,为什么现在也可以用在服务器?因为可以进行IO操作,Input(输入)/Output(输出), js做不了的事情,就会交给libuv去完成,libuv完成后把结果和回调放到队列里,js再从事件循环里取出来

但是一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers) :本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调(Pending Callback) :对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到 ECONNREFUSED。
  • idle, prepare:仅系统内部使用。
  • 轮询(Poll) :检索新的 I/O 事件;执行与 I/O 相关的回调;
  • 检测(check) :setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。

16.7. Node事件循环的阶段图解

image-20220323132628207.png

16.8. Node的宏任务和微任务

我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

但是,Node中的事件循环不只是 微任务队列和 宏任务队列:

  • 微任务队列:(若队列里两种微任务都存在,则按下面排列顺序执行)

    1. next tick queue:process.nextTick;
    2. other queue:Promise的then回调、queueMicrotask;
  • 宏任务队列: (在一次Tick,会按下面排列顺序执行宏任务)

    1. timer queue:setTimeout、setInterval;
    2. poll queue:IO事件;
    3. check queue:setImmediate;
    4. close queue:close事件;

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

  • next tick microtask queue;
  • other microtask queue;
  • timer queue;
  • poll queue;
  • check queue;
  • close queue;

16.9. node面试题(困难)

 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('setTimeout0')
 }, 0)
 // 注意:等到300ms后才会加入到timer queue,也就是所有的宏任务都执行了,才会执行这个回调
 setTimeout(function () {
   console.log('setTimeout2')
 }, 300)
 ​
 setImmediate(() => console.log('setImmediate'));
 ​
 process.nextTick(() => console.log('nextTick1'));
 ​
 async1();
 ​
 process.nextTick(() => console.log('nextTick2'));
 ​
 new Promise(function (resolve) {
   console.log('promise1')
   resolve();
   console.log('promise2')
 }).then(function () {
   console.log('promise3')
 })
 ​
 console.log('script end')
 // script start
 // async1 start
 // async2
 // promise1
 // promise2
 // script end
 // nexttick1
 // nexttick2
 // async1 end
 // promise3
 // settimetout0
 // setImmediate
 // setTimeout2