深入理解 JavaScript 中的 Event Loop(事件循环)

214 阅读8分钟

JavaScript 是一种单线程、非阻塞、事件驱动的编程语言。这意味着它一次只能执行一个任务,并且不会等待任务完成,而是通过事件驱动的方式来处理异步操作。理解 JavaScript 的事件循环机制对于编写高效的异步代码至关重要。本文将深入探讨 JavaScript 中的事件循环,包括宏任务和微任务,以及它们是如何影响代码执行顺序的。

进程与线程

在计算机科学中,进程和线程是两个重要的概念。

  • 进程:是程序的一次执行过程,是系统进行资源分配和调度的一个独立单位。每个进程都有自己的内存空间、数据栈以及其他资源。
  • 线程:是进程中的一个执行单元,描述了一段指令执行所需的时间。一个进程可以包含多个线程,它们共享进程的资源。

JavaScript 单线程

与许多其他语言不同,JavaScript 是一种单线程语言,意味着它只有一个执行线程。这一设计初衷是为了简化编程模型,避免多线程编程中的诸多问题,比如死锁和竞态条件。单线程的优点包括节省内存和避免锁,但也限制了其并行处理能力。

为什么使用单线程?

JavaScript 的单线程特性与其设计初衷和应用环境紧密相关。最关键的是,JavaScript 的单线程特性与浏览器的渲染机制用户交互体验以及 事件驱动模型密切相关。

1. 与浏览器渲染机制的关系

JavaScript 最初被设计用于浏览器的前端开发,其主要目标是操作网页的 DOM 以及响应用户事件。因为浏览器需要同时负责渲染网页、响应用户操作以及执行 JavaScript 代码,如果 JavaScript 是多线程的,可能会带来以下问题:

  • 资源竞争问题:多个线程同时操作 DOM 或修改页面元素可能会导致数据不一致或冲突。假设两个线程同时修改同一个 DOM 节点,会产生不可预期的结果。
  • 页面渲染冲突:如果多个线程同时操作页面渲染,可能导致渲染的顺序不确定,用户看到的页面会出现混乱或卡顿。

因此,为了避免这些问题,JavaScript 被设计为单线程,确保一次只执行一个任务,从而避免了这些复杂的竞争问题。

2. 与用户体验和交互的关系

JavaScript 主要用于与用户交互,因此单线程的设计有助于保证用户界面的稳定和流畅性。

  • 同步性:单线程确保事件按照顺序发生,用户操作后能立即响应,不会因为多个线程的竞争导致响应延迟或错乱。
  • 简化开发:单线程的模型也减少了开发者在处理并发时可能遇到的复杂性,使得前端代码更容易编写和调试。

为了避免单线程造成的阻塞问题(例如,当执行长时间任务时,页面会挂起),JavaScript 引入了异步机制(如事件循环、Promise、async/await),以保证即便是单线程,长时间任务也不会阻塞用户界面响应。

3. 与事件驱动模型的关系

JavaScript 采用事件驱动的模型,核心就是通过事件循环(Event Loop)来调度任务,而单线程在这种事件驱动模型中简化了任务的管理:

  • 事件循环让 JavaScript 的单线程能够处理多个任务,像用户交互、定时器、网络请求等异步任务都会通过回调机制来执行,尽管主线程只执行一个任务,但通过事件循环,可以在任务完成后继续执行其他任务。
4. 与Node.js的关系

在服务器端的 Node.js 中,JavaScript 依然是单线程的。这是因为 Node.js 也采用了事件驱动和异步 I/O 模型,这种设计方式让 Node.js 即便是单线程,也能够高效地处理大量并发请求。

  • 异步 I/O:通过非阻塞的 I/O 操作,Node.js 可以在等待文件读写或数据库请求的同时继续处理其他请求,从而提高性能。
  • 多线程与单线程协作:尽管 Node.js 的主线程是单线程的,它依然可以通过 C++ 底层的线程池(如 libuv 库)来执行一些耗时的操作(如加密计算、文件读写),从而在一定程度上实现并行处理。
总结

JavaScript 的单线程与以下几个方面有密切关系:

  • 浏览器的渲染机制:避免线程竞争和渲染冲突,确保 DOM 操作和页面更新的安全性。
  • 用户交互体验:单线程避免了复杂的并发处理,使得用户界面的更新和事件响应能够顺序执行,提升了用户体验。
  • 事件驱动模型:通过事件循环和异步机制,单线程 JavaScript 可以同时处理多个任务。
  • Node.js 的异步 I/O 模型:单线程配合异步 I/O 机制和事件驱动,使得 Node.js 可以高效处理高并发请求。

通过异步机制和事件驱动模型,JavaScript 的单线程不仅避免了多线程带来的复杂问题,还能保持较高的性能。

异步编程

尽管 JavaScript 是单线程的,但它支持异步编程模型。通过回调函数、Promise、async/await 等机制,JavaScript 可以处理异步操作,而不会阻塞主线程的执行。异步操作允许代码在等待某些操作完成时继续执行其他任务,从而提高了程序的响应性和性能。

宏任务与微任务

在 JavaScript 中,异步任务可以分为两种类型:宏任务和微任务。

  • 宏任务(macrotask):代表一个离散的工作单元,比如 setTimeout、setInterval、I/O 操作、UI 渲染等。宏任务会被添加到任务队列(task queue)中等待执行。
  • 微任务(microtask):比宏任务更小的任务单元,例如 Promise 的 then 方法、MutationObserver 等。微任务会在当前任务执行完毕后立即执行,而不需要等待事件循环的下一个轮次。

事件循环(Event Loop)

JavaScript 的事件循环机制决定了代码执行的顺序。事件循环可以分为以下几个步骤:

  1. 执行同步代码:首先执行调用栈中的同步代码,即当前执行上下文中的代码。
  2. 处理异步任务:当调用栈为空时,事件循环开始查询是否有异步任务需要执行。它会优先执行微任务队列中的任务,直到微任务队列为空。
  3. 渲染页面:如果需要,会在这个阶段渲染页面,更新用户界面。
  4. 执行宏任务:一旦微任务队列为空,事件循环开始执行宏任务队列中的第一个任务 (这里需要注意,仅仅执行第一个宏任务,而并非所有的宏任务队列内的任务)
  5. 下一轮事件循环:以上步骤循环进行,构成了 JavaScript 的事件循环机制。(如果宏任务或者微任务队列没有任务,则任务队列会进入休眠状态,直接执行其他有任务队列的任务)

简而言之 先执行同步任务 然后执行微任务 再执行宏任务 ......

案例

请你写出下列代码的打印顺序

console.log('script start')
async function async1() {
    await async2()//==.then(async2())原本的用法    
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
}
async1()
setTimeout(function () {
    console.log('setTimeout')
}, 0)
new Promise(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function () {
        console.log('promise1')
    })
    .then(function () {
        console.log('promise2')
    })
console.log('script end')

执行顺序为

image.png

首先执行同步任务console.log('script start')

接着执行async1(),由于在async1内部使用await,所以await后面的任务全部成为微任务。

虽然async2()使用async关键字声明(aysnc声明是同步的),但是因为内部console.log('async2 end')没有用await关键字返回,所以没有被作为异步任务,因此console.log('async2 end')作为同步任务被打印了。

async声明的函数的返回promise是微任务,所以和后面的 console.log('async1 end')放入微任务队列。

由于async1是异步任务,于是console.log('async1 end')放在微任务队列中。

前面所述setTimeout()是宏任务,因此放入宏任务队列

接着执行同步任务Promise() 我们需要注意,new Promise()是一个同步任务,只有.then()的时候才是微任务,因此 .then放在微任务队列

接着执行同步任务console.log('script end')

目前已执行:console.log('script start') console.log('async2 end')``console.log('Promise') console.log('script end')

微任务队列:console.log('async1 end')`` console.log('promise1') console.log('promise2')

宏任务队列: console.log('setTimeout')

接下来按顺序执行微任务队列和宏任务队列,就可以得到上面的结果啦

总结

JavaScript 的事件循环机制决定了代码的执行顺序,它通过处理宏任务和微任务来实现异步编程。理解事件循环是成为一名高效 JavaScript 开发者的重要基础之一。通过合理地利用宏任务和微任务,我们可以编写出更加响应迅速、性能优异的 JavaScript 代码。