一篇文章教会你 JS 中的事件循环机制

365 阅读5分钟

JS 中的事件循环机制

引言

JavaScript 是一种单线程的语言,这意味着它在同一时间只能执行一个任务。然而,在现代 Web 开发中,我们需要处理许多异步操作,如网络请求、定时器等。为了管理这些异步操作并保持代码的执行效率,JavaScript 引入了事件循环机制,以及区分宏任务和微任务的概念。本文将深入探讨这些概念,并通过示例代码帮助理解它们是如何工作的。

核心知识点

1. 同步与异步

JavaScript 的执行模型分为同步任务和异步任务。同步和异步是计算机编程中两种不同的执行模式。同步任务会直接在主线程上执行,而异步任务则会被放入一个队列中等待执行。

  • 同步:在同步操作中,程序的执行会阻塞或者等待一个任务完成之后再继续向下执行。这意味着调用者必须等待直到被调用的任务完成才能继续下一步操作。这种模式简单直接,但在处理耗时操作时会导致性能瓶颈。

  • 异步:在异步操作中,程序不需要等待一个任务完成就可以继续执行其他任务。一旦启动了一个异步操作,调用者可以立即进行下一个操作,而不会被阻塞。当异步操作完成时,通常会通过回调函数、事件通知或者 Promise(对于支持的语言如 JavaScript)来告知结果。

2. 宏任务与微任务

  • 宏任务:包括 全局执行上下文setTimeoutsetInterval 等,这些任务会在当前的执行栈清空之后执行。

  • 微任务:包括 Promise.then()process.nextTick()MutationObserver()await 后面的代码 等,这些任务会在当前宏任务的执行栈清空之后立即执行,但在此之前所有微任务都会先执行完毕。

注意:在微任务队列中,process.nextTick()的优先级最高,最先会从微任务队列中拿出来执行。

3. 事件循环机制

JavaScript 的执行引擎会不断从任务队列中取出任务来执行,这个过程被称为事件循环。每次事件循环包括以下几个阶段:

  1. 执行栈:执行同步任务,直到执行栈为空。
  2. 微任务队列:执行所有的微任务,直到队列为空。
  3. 宏任务队列:如果还有宏任务,则开始下一个宏任务的执行,从而开启新的事件循环。

4. 事件循环的执行流程

  1. 执行同步代码:首先执行所有同步代码。
  2. 检查异步任务:同步代码执行完毕后,检查是否有异步任务需要执行。
  3. 执行所有的微任务:在执行下一个宏任务之前,执行所有等待中的微任务。
  4. 渲染页面:如果需要,执行用户界面的渲染。
  5. 执行宏任务:执行等待中的宏任务,从而开始下一轮事件循环。

5. 示例代码分析

下面是一个具体的例子来说明上述概念:

console.log('script start');

async function async1() {
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2 end');
}

async1();

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

new Promise(function(resolve, reject) {
  console.log('promise');
  resolve();
})
.then(() => {
  console.log('then1');
})
.then(() => {
  console.log('then2');
});

console.log('script end');

(1)输出结果预测

  1. script start
  2. async2 end
  3. promise
  4. script end
  5. async1 end
  6. then1
  7. then2
  8. setTimeout

(2)解析

  1. 同步代码

    • console.log('script start'); 是第一个被执行的同步代码,输出 script start
  2. 异步代码

    • async1() 被调用,其中 await async2(); 会等待 async2() 执行完毕。
    • async2() 打印出 async2 end
    • async1() 继续执行,打印出 async1 end
  3. 微任务

    • Promise.resolve().then(() => {...}) 是一个微任务,它会在当前宏任务结束之后立即执行,因此 then1 和 then2 会紧接着输出。
  4. 宏任务

    • console.log('script end'); 是同步代码的一部分,所以接下来输出 script end
    • setTimeout 是一个宏任务,尽管它的延时为0,但需要等待当前宏任务完成及所有微任务执行完毕后才会执行。因此最后输出 setTimeout

(4)具体步骤

  1. 执行同步代码

    • 输出 script start
  2. 执行 async1()

    • 这个函数被调用,但由于 await 的存在,它会等待 async2() 的执行。
    • async2() 被调用,输出 async2 end
  3. 执行 Promise 构造函数

    • 创建一个新的 Promise 对象并在构造函数中立即执行,输出 promise
    • Promise 对象的 .then() 方法被注册,但不会立即执行。
  4. 执行同步代码

    • 输出 script end
  5. 执行微任务

    • Promise 的 .then() 方法作为微任务开始执行,输出 then1 和 then2
  6. 执行宏任务

    • setTimeout 作为宏任务执行,输出 setTimeout

总结

JavaScript 的事件循环机制是其异步处理的核心。理解宏任务和微任务的区别对于编写高效、可靠的代码至关重要。通过本文,我们不仅深入了解了 JavaScript 中的事件循环机制,还学习了如何区分宏任务和微任务,并通过实际代码示例加深了理解。通过本篇文章的学习,希望你能够更加熟练地掌握 JavaScript 的异步编程技巧。