面试必会js事件循环机制( JavaScript事件循环机制详解与面试输出题分析)

JavaScript事件循环机制详解与面试题分析

写在前面

一面挂*n,好几次输出题死在此类题目上,每次都以为学会了每次都错出新花样,终于是觉得自己不会再错了,于是想写一篇来谈谈这块内容(并且给出一些面试题,自测是否熟练掌握这个知识点,不要像我一样再死在这知识点了)

概念

JavaScript 是单线程语言,通过 事件循环(Event Loop) 协调异步任务的执行,其核心机制如下:

1. 任务分类

  • 同步任务:直接在执行栈中同步执行。
  • 异步任务:
    • 宏任务:如 setTimeout、setInterval、I/O(Node.js)、UI 渲染(浏览器)等,存于宏任务队列(分阶段或类型排队)。
    • 微任务:如 Promise.then、MutationObserver、process.nextTick(Node.js)等,存于微任务队列(全局唯一队列)。

提示:重点关注是promise.then!!!然后和setTimeout比较常考
任务分类

2. 执行流程

  • 先执行同步任务,同步任务执行完毕后,进入事件循环。
  • 每轮事件循环:
    1. 清空微任务队列中所有微任务(按添加顺序,直至队列为空)。
    2. 从宏任务队列中取出队头宏任务执行。
  • 重复上述步骤,直至所有队列清空。执行流程

好用的技巧

在做输出题时,列三行,给宏任务,微任务留够足够大的空间~

  • 输出栈
  • 宏任务
  • 微任务

用法:模拟出入队列!!!一定要写在纸上,心算易出错

示例代码分析

下列是一个看起来很复杂的多层嵌套代码块:

console.log('【同步】start');

// 第一层:同步任务中注册宏任务和微任务
setTimeout(() => { // 宏任务1(定时器队列,第1个宏任务)
  console.log('\n【宏任务1开始】');

  // 宏任务1中注册微任务1
  Promise.resolve().then(() => {
    console.log('【微任务1-1】在宏任务1中注册');
    
    // 微任务1-1中注册微任务1-2
    Promise.resolve().then(() => {
      console.log('【微任务1-2】在微任务1-1中注册');
      
      // 微任务1-2中注册宏任务3
      setTimeout(() => { // 宏任务3(定时器队列,第3个宏任务)
        console.log('\n【宏任务3开始】');
        
        // 宏任务3中注册微任务3-1
        Promise.resolve().then(() => {
          console.log('【微任务3-1】在宏任务3中注册');
        });
        
        // 宏任务3中注册宏任务4
        setTimeout(() => { // 宏任务4(定时器队列,第4个宏任务)
          console.log('\n【宏任务4开始】');
        }, 0);
      }, 0);
    });

    // 微任务1-1中注册宏任务2
    setTimeout(() => { // 宏任务2(定时器队列,第2个宏任务)
      console.log('\n【宏任务2开始】');
      
      // 宏任务2中注册微任务2-1
      Promise.resolve().then(() => {
        console.log('【微任务2-1】在宏任务2中注册');
      });
    }, 0);
  });

  // 宏任务1中注册同步任务(模拟耗时操作)
  for (let i = 0; i < 1000000; i++) { /* 模拟耗时同步操作 */ }
  console.log('【宏任务1同步部分】执行完毕');

}, 0);

// 同步任务中注册微任务0
Promise.resolve().then(() => { // 微任务0(第1个微任务)
  console.log('\n【微任务0开始】');
  
  // 微任务0中注册微任务0-1
  Promise.resolve().then(() => {
    console.log('【微任务0-1】在微任务0中注册');
    
    // 微任务0-1中注册微任务0-2(嵌套三层微任务)
    Promise.resolve().then(() => {
      console.log('【微任务0-1-2】在微任务0-1中注册');
    });
  });

  // 微任务0中注册宏任务5(定时器队列,第5个宏任务)
  setTimeout(() => { // 宏任务5
    console.log('\n【宏任务5开始】');
  }, 0);
});

console.log('【同步】end'); // 同步任务结束

纸上分析(推荐代码在vscode打开,可以折叠看清楚层次):

在这里插入图片描述

  1. 执行同步代码!!!第1行,第68行为同步代码;4-46行为宏任务,49-66为微任务。
  • 输出栈:【同步】start,【同步】end
  • 宏任务:4-46行代码
  • 微任务:49-66行代码
  1. 进入第一个事件循环,清空微任务队列,则运行49-66行(我这里折叠看得清楚层次)此时这里同步代码为第50行,53-60放入微任务队列,63-65放入宏任务队列在这里插入图片描述
  • 输出栈:【同步】start,【同步】end,【微任务0开始】
  • 宏任务:4-46行代码;63-65行代码
  • 微任务:53-60行

此时微任务队列并没有清空,所以继续执行微任务队列代码53-60行,53-60行里54为同步代码立即执行,57-59为微任务放入微任务队列
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册
  • 宏任务:4-46行代码;63-65行代码
  • 微任务:57-59行

此时微任务队列并没有清空,所以继续执行微任务队列代码57-59行,57-59行里58为同步代码立即执行
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册
  • 宏任务:4-46行代码;63-65行代码
  • 微任务:

此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行4-46行代码。此时可见第5行和第44行为同步代码,8-40行为微任务放入微任务队列
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕
  • 宏任务:63-65行代码
  • 微任务:8-40行代码
  1. 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码8-40行。8-40行内第9行为同步代码,12-29是微任务放入微任务队列,32-39为宏任务放入宏任务队列
    在这里插入图片描述
  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册
  • 宏任务:63-65行代码;32-39行代码
  • 微任务:12-29行代码

此时微任务队列并没有清空,所以继续执行微任务队列代码12-29行,12-29行里13为同步代码立即执行,16-28为宏任务放入宏任务队列
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册
  • 宏任务:63-65行代码;32-39行代码;16-28行代码
  • 微任务:

此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行63-65行代码。此时可见第64行为同步代码直接输出
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】
  • 宏任务:32-39行代码;16-28行代码
  • 微任务:
  1. 进入下一个事件循环:微任务队列和宏任务并没有清空,微任务队列被清空了,所以取出宏任务队列队头32-39行执行。第33行为同步代码直接输出,第36-38为微任务放入微任务队列
    在这里插入图片描述
  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】
  • 宏任务:16-28行代码
  • 微任务:36-38行
  1. 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码36-38行。37行为同步代码直接执行
    在这里插入图片描述
  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册
  • 宏任务:16-28行代码
  • 微任务:

此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行16-28行代码。此时可见第17行为同步代码直接输出,第20-22为微任务放入微任务队列,第25-27为宏任务放入宏任务队列
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】
  • 宏任务:25-27行代码
  • 微任务:20-22行代码
  1. 进入下一个事件循环:微任务队列和宏任务并没有清空,所以需要清空微任务队列代码20-22行。21行为同步代码直接执行
  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】,【微任务3-1】在宏任务3中注册
  • 宏任务:25-27行代码
  • 微任务:

此时微任务队列清空,宏任务队列没有清空,取出宏任务队头执行,则执行25-27行代码。此时可见第26行为同步代码直接输出。
在这里插入图片描述

  • 输出栈:【同步】start,【同步】end,【微任务0开始】,【微任务0-1】在微任务0中注册,【微任务0-1-2】在微任务0-1中注册,【宏任务1开始】,【宏任务1同步部分】执行完毕,【微任务1-1】在宏任务1中注册,【微任务1-2】在微任务1-1中注册,【宏任务5开始】,【宏任务2开始】,【微任务2-1】在宏任务2中注册,【宏任务3开始】,【微任务3-1】在宏任务3中注册,【宏任务4开始】
  • 宏任务:
  • 微任务:
  1. 宏任务队列和微任务队列为空,执行完毕!
    在这里插入图片描述

最终输出结果:

【同步】start
【同步】end

【微任务0开始】
【微任务0-1】在微任务0中注册
【微任务0-1-2】在微任务0-1中注册

【宏任务1开始】
【宏任务1同步部分】执行完毕
【微任务1-1】在宏任务1中注册
【微任务1-2】在微任务1-1中注册

【宏任务5开始】

【宏任务2开始】
【微任务2-1】在宏任务2中注册

【宏任务3开始】
【微任务3-1】在宏任务3中注册

【宏任务4开始】

面试真题解析

美团一面4.08

let first = new Promise((resolve, reject) => {
    console.log("1");
    let p = new Promise((resolve, reject) => {
        console.log("6");
        setTimeout(() => {
            console.log("5");
            resolve(1);
        }, 0);
        resolve(3);
    });
    resolve(2);
    p.then((val) => {
        console.log(val);
    });
});
first.then((val) => {
    console.log(val);
});
console.log('4');

答案:1,6,4,3,2,5

解析

  1. 同步代码!!!注意 new一个promise只要不执行then都是同步代码!!!同步代码第2行、第4行、第9行、第11行、第19行执行,则输出1,6,4;p状态变为fulfilled值为第9行放入的3,first状态变为fulfilled值为第11行放入的2,但不输出,等回调函数then执行才输出。
  • 输出栈:1,6,4
  • 宏任务:5-8行
  • 微任务:12-14行;16-18行
  1. 第一次循环:
    清空微任务,执行12-14行;16-18行,依次输出p、first的fulfilled的值3,2
  • 输出栈:1,6,4,3,2
  • 宏任务:5-8行
  • 微任务:

宏任务执行,此时因为p已经fulfilled所以第7行无效不输出(可以看promise定义,一旦改变状态不会再变化),输出第6行的代码。

  • 输出栈:1,6,4,3,2,5
  • 宏任务:
  • 微任务:
  1. 最终输出:1,6,4,3,2,5

滴滴一面

function log1() {
    setTimeout(() => {
        console.log(1)
    }, 0)
}
function log2() {
    console.log(2)
}
function log3() {
    console.log(4)
    return new Promise((reslove) => {
        console.log(5)
        reslove(6)
    })
}
log1()
log2()
log3().then(res => { console.log(res) })

答案:2,4,5,6,1

解析:log1(),log2(),log2()同步代码,其他同上流程自己列列~

腾讯云智一面5.14

new Promise(resolve => {
    console.log(1);
    resolve(2);
    setTimeout(() => {
      Promise.resolve().then(() => console.log(7))
      console.log(3);
    }, 0)
    Promise.resolve().then(() => console.log(4))
  }).then(num => {
    console.log(num)
  });

  setTimeout(() => {
    console.log(5)
  }, 0)

  console.log(6);

答案:1,6,4,2,3,7,5

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值