Js 的事件循环(Event Loop)机制以及面试题讲解

你是否曾在面试中被问及JavaScript的事件循环机制?是否曾对setTimeout的执行时机感到困惑?或者对Promise和setTimeout的优先级一头雾水?别担心,今天我们就来彻底解决这些问题!

为什么JS是单线程?

想象一下这样的场景:如果JavaScript是多线程的,一个线程要在某个DOM节点上添加内容,另一个线程却要删除这个节点,浏览器该听谁的呢?这就好比两个人同时试图驾驶一辆车,一个想左转一个想右转,结果可想而知。

为了避免这种混乱,JavaScript从诞生起就是单线程的。这意味着它一次只能做一件事,所有任务都需要排队执行。这种设计简化了编程难度,但同时也带来了一个问题:如果某个任务特别耗时,后续所有任务都会被阻塞,导致页面"卡死"。

执行栈与任务队列

执行栈

执行栈就像是一个装盘子的容器------最后放进去的盘子会被最先拿出来(后进先出)。当JavaScript开始执行代码时,它会创建一个全局执行上下文,并将其压入执行栈中。每当调用一个函数,就会创建一个新的函数执行上下文并压入栈顶。函数执行完毕后,它的执行上下文就会从栈中弹出。

javascript 复制代码
function a() {
  console.log('a');
  b();
}

function b() {
  console.log('b');
}

a();
// 执行顺序:a -> b

主线程

主线程是JavaScript执行同步代码的地方。所有同步任务都在这里按顺序执行,形成一个执行栈。当遇到异步操作时,JavaScript不会等待它完成,而是继续执行后面的代码。

JS异步执行的运行机制

既然JavaScript是单线程的,那它如何处理Ajax请求、定时器这类异步操作呢?答案就是:借助浏览器的多线程能力。

当JavaScript遇到异步操作时,会将它交给浏览器的其他线程(如定时器线程、HTTP请求线程等)处理,然后继续执行后面的同步代码。当异步操作完成后,相应的回调函数会被放入任务队列中等待执行。

这就好比在咖啡店点单:你点了一杯咖啡(发起异步请求),然后去找座位(继续执行同步代码),当咖啡做好后(异步操作完成),服务员会叫你的名字(回调函数进入任务队列),但你只有在空闲时(执行栈为空)才会去取咖啡(执行回调函数)。

宏任务与微任务

任务队列并非只有一个,而是分为两种类型:

宏任务(MacroTask) :包括setTimeout、setInterval、setImmediate(Node.js)、I/O操作、UI渲染等

微任务(MicroTask) :包括Promise.then/catch/finally、process.nextTick(Node.js)、MutationObserver等

它们的执行顺序很重要:当执行栈为空时,会先检查并清空所有微任务,然后才取出一个宏任务执行,然后再次检查微任务

Event Loop(事件循环)

事件循环是JavaScript实现异步的核心机制,它的工作流程可以用以下代码简单表示:

scss 复制代码
while (true) {
  // 执行栈为空时开始循环
  if (执行栈为空) {
    // 1. 检查微任务队列,执行所有微任务
    while (微任务队列中有任务) {
      执行微任务();
    }
    
    // 2. 取出一个宏任务执行
    if (宏任务队列中有任务) {
      执行宏任务();
    }
  }
}

这个过程就像是一个永不停歇的循环,不断检查执行栈和任务队列,确保任务有序执行。

面试题实践

现在让我们通过一些面试题来巩固理解:

题目1:基本执行顺序

javascript 复制代码
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

// 输出顺序:1 -> 4 -> 3 -> 2

解析

  1. 同步代码:先输出1和4
  2. 微任务:Promise.then是微任务,输出3
  3. 宏任务:setTimeout是宏任务,最后输出2

题目2:嵌套异步操作

javascript 复制代码
console.log('start');

setTimeout(() => {
  console.log('timeout');
}, 0);

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

console.log('end');

// 输出顺序:start -> end -> promise1 -> promise2 -> timeout

题目3:综合挑战

javascript 复制代码
console.log('1');

setTimeout(function() {
  console.log('2');
  new Promise(function(resolve) {
    console.log('3');
    resolve();
  }).then(function() {
    console.log('4');
  });
}, 0);

new Promise(function(resolve) {
  console.log('5');
  resolve();
}).then(function() {
  console.log('6');
});

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

console.log('8');

// 输出顺序:1 -> 5 -> 8 -> 6 -> 2 -> 3 -> 4 -> 7

解析

  1. 同步代码:输出1、5、8(Promise构造函数是同步执行的)
  2. 微任务:输出6
  3. 第一个setTimeout(宏任务):输出2、3(Promise构造函数同步执行)
  4. 第一个setTimeout中的then(微任务):输出4
  5. 第二个setTimeout(宏任务):输出7

总结

JavaScript的事件循环机制是前端开发中的核心概念,理解它对于编写高效、无阻塞的代码至关重要。记住几个关键点:

  1. JavaScript是单线程的,但借助浏览器的多线程能力实现异步
  2. 任务分为同步任务和异步任务,异步任务又分为宏任务和微任务
  3. 事件循环的顺序:同步任务 > 微任务 > 宏任务
  4. 微任务优先级高于宏任务,且会在当前宏任务结束后立即执行

希望这篇文章能帮助你彻底理解JavaScript的事件循环机制,下次面试时再遇到相关问题,就能从容应对了!

相关推荐
小满xmlc2 小时前
react Diff 算法
前端
bug_kada2 小时前
深入理解 JavaScript 可选链操作符
前端·javascript
小满xmlc2 小时前
CI/CD 构建部署
前端
_AaronWong2 小时前
视频加载Loading指令:基于Element Plus的优雅封装
前端·electron
KallkaGo2 小时前
threejs复刻原神渲染(三)
前端·webgl·three.js
IT_陈寒4 小时前
Vue3性能优化:掌握这5个Composition API技巧让你的应用快30%
前端·人工智能·后端
excel13 小时前
在 Node.js 中用 C++ 插件模拟 JavaScript 原始值包装对象机制
前端
excel16 小时前
应用程序协议注册的原理与示例
前端·后端
我是天龙_绍18 小时前
浏览器指纹,一个挺实用的知识点
前端