前端面试题 - 3. 说说事件循环机制

267 阅读5分钟

浏览器事件循环

  • 原理:Event Loop(事件循环)中,每一次循环称为 tick, 每轮循环都是由一个宏任务+多个微任务组成

    1. 首先,执行第一个宏任务:全局Script脚本。
    2. 产生的的宏任务和微任务进入各自的队列中。
    3. 执行完Script后,把当前的微任务队列清空。完成一次事件循环。
    4. 接着再取出一个宏任务,同样把在此期间产生的回调入队。再把当前的微任务队列清空。以此往复。
  • 原因:JS是单线程的,一次只能做一件事情。为了不阻塞用户交互。JavaScript程序采用了异步事件驱动编程(Event-driven programming)模型(通过交互驱动)。异步事件驱动编程实现原理就是事件循环(Event Loop)。

    • 同步异步:在等待异步任务准备的同时,JS引擎去执行其他同步任务,等到异步任务准备好了,再去执行回调。
    • 事件循环:异步任务的回调部分需要在合适时机交还给JS线程执行,这就需要事件通知。任务是由一个队列组成的,异步任务的回调遵循先进先出,在JS引擎空闲时会一轮一轮地被取出,所以被叫做循环。
  • 任务:队列中任务的不同,分为宏任务和微任务

    • 宏任务:script代码,setTimout,setInterval,setImmediate,UI 渲染、 I/O、postMessage、 MessageChannel
      • setTimeout 0ms 也不是立刻执行,它有一个默认最小时间,为4ms。
    • 微任务:promise,process.nextTick,MutationObserver

node端事件循环

node的事件循环由6个宏任务队列+6个微任务队列组成。

  1. Timers:定时器setTimeout/setInterval回;

  2. I/O回调

  3. Idle,prepare

  4. Poll :获取新的 I/O 事件, 例如操作读取文件等;

  5. Check:setImmediate回调;

  6. close回调

其执行规律是:在一个宏任务队列全部执行完毕后,去清空一次微任务队列,然后到下一个等级的宏任务队列,以此往复。一个宏任务队列搭配一个微任务队列。

除此之外,node端微任务也有优先级先后:

  1. process.nextTick;
  2. promise.then 等;

async/await的执行顺序

async,await: 是建立在 promise 的基础上的 ,一般成对出现。当一个函数前加了async,就表示这个函数里很大可能有异步函数,而在第一个await表达式出现之前,异步函数内部的代码都是按照同步方式执行的。await所在表达式是同步的。

async function fn1(){
     console.log(1)
     await fn2()
     console.log(3)
 }
async function fn2(){
     console.log('fn(2)')
 }
 fn1()
 console.log(2)
 
//结果:1 fn2() 2 3

实例1:如下代码输出顺序是什么?

console.log('script start');


setTimeout(function() {
  console.log('setTimeout');
}, 0);


Promise.resolve()
.then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});


console.log('script end');

答案

解读:

宏任务1
    按顺序执行:script start
        promise的微任务入队
    按顺序执行:script end
        promise的微任务出队
    按顺序执行:输出promise1
    按顺序执行:输出promise2
setTimeout作为第二个宏任务
    输出setTimeout

实例2:如下代码输出顺序是什么?

async function test1() {
  console.log('start test1');
  console.log(await test2());
  console.log('end test1');
}
async function test2() {
  console.log('test2');
  return await 'return test2 value';
}
test1();
console.log('start async');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});
console.log('end async');


//输出
/* 
start test1
test2
start async
promise1
end async
promise2
return test2 value
end test1
setTimeout
*/

解读:需要注意的是promise2 => end test1。 原因是:return await是第二层的微任务了,所以是它的入队会在Promise后面。

实例3:如下代码输出顺序是什么?

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();
  console.log('promise2')
}).then(function () {
  console.log('promise3');
});
console.log('script end');

输出
script start
async1 start
async2
promise1
promise2
script end
async1 end
promise3
setTimeout

解读

宏任务1:
    按顺序执行:script start
        setTimeout宏任务入队
    async1函数顺序执行:async1 start
    await行的代码同步执行:async2。函数中没有异步操作,等价于直接执行
        await后的微任务入队
    new Promise内的代码同步执行
        promise1
        resolve执行
        执行promise2
        Promise微任务入队
    同步执行后面的代码:script end
        Promise微任务出队
    微任务按顺序出队
        await后的微任务出队:async1 end
        promise出队执行:promise3
宏任务2setTimeout  

宏任务和微任务的区分是为了做什么? 优先级?

宏任务和微任务的区分是为了执行 JavaScript 代码中的异步操作。在 JavaScript 中,异步操作可以使用回调函数、Promise、async/await 等方式实现。

宏任务和微任务的优先级是不同的,微任务的优先级高于宏任务。当 JavaScript 引擎执行完一个宏任务时,会检查是否有微任务需要执行,如果有,它会先执行微任务队列中的任务,直到微任务队列为空,然后再继续执行下一个宏任务。

这个机制的设计是为了保证 JavaScript 代码的正确性和性能。例如,当我们使用 Promise 来处理异步操作时,可以将 then 方法中的代码作为微任务,这样可以保证在 Promise 完成后立即执行 then 方法中的代码,而不需要等待下一个宏任务的执行。这样可以提高代码的响应速度和性能。

参考资料:

  1. 事件循环机制:juejin.cn/post/684490…
  2. 为什么要这样设计:juejin.cn/post/707309…
  3. 事件循环示例:blog.csdn.net/znhyXYG/art…