Jansiel Notes

JavaScript事件循环机制和异步处理

前言

JavaScript是一门单线程的编程语言,这意味着它一次只能执行一个任务。然而,为了处理异步操作和提高性能,JavaScript引入了事件循环(Event Loop)的概念。本文将深入探讨JavaScript事件循环的原理、异步编程、浏览器多线程以及与事件循环相关的例子。

在深入研究JavaScript事件循环和异步处理之前,让我们先了解一下计算机科学中的一些基本概念,如进程和线程。

进程与线程

进程是计算机中执行的程序的实例,拥有独立的内存空间和系统资源。而线程是进程中的执行单元,多个线程可以共享进程的内存空间。这种分层的结构使得操作系统可以更有效地管理和调度运行中的程序。

浏览器多线程

在Web开发中,浏览器扮演着执行JavaScript代码的角色。当我们打开一个新的标签页时,通常会创建一个新的进程,这被称为进程隔离。在进程内部,存在多个线程,每个线程负责不同的任务:

  • 渲染线程:负责页面的渲染,解析HTML和CSS,构建DOM树和渲染页面。
  • HTTP请求线程:处理网络请求。
  • JS引擎线程:执行JavaScript代码。

这些线程之间可以协同工作,但在浏览器中,渲染线程和JS引擎线程是互斥的,以确保操作的原子性。

JavaScript的单线程执行

JavaScript被设计为单线程执行的语言,这主要是为了避免复杂的同步和竞争条件问题。虽然这可能导致一些性能方面的挑战,但也带来了一些优势。

优点

  • 节约内存:不需要考虑多线程之间的共享数据和同步问题,节省了内存。
  • 没有锁的概念:避免了锁的使用,减少了上下文切换的时间。

JavaScript的单线程执行模型意味着在同一时刻,只能有一个任务在执行。这为JavaScript提供了独特的特性,尤其适用于处理与用户界面相关的操作。

异步编程和事件循环

在单线程的JavaScript中,异步编程变得至关重要。异步操作允许程序在等待某些操作完成的同时执行其他任务,以提高性能和响应速度。JavaScript中的异步操作分为宏任务和微任务。

宏任务和微任务

  • 宏任务(macrotask):包括整体代码(script)、setTimeout、setInterval、I/O等。
  • 微任务(microtask):包括Promise.then()、MutationObserver等。

让我们通过一个具体的例子来更好地理解宏任务和微任务的概念:

 1console.log('Start');
 2
 3setTimeout(function () {
 4    console.log('Timeout');
 5}, 0);
 6
 7Promise.resolve().then(function () {
 8    console.log('Promise');
 9});
10
11console.log('End');
12

在这个例子中,整体代码、setTimeout和Promise都是宏任务,而Promise.then()是微任务,需要注意的是Promise.then()才是微任务,Promise不是微任务。执行顺序将是"Start" -> "End" -> "Promise" -> "Timeout"。微任务会在下一轮事件循环开始之前执行。

Event Loop的执行过程

事件循环是JavaScript异步编程的基础,确保了代码的执行顺序和一致性。事件循环的执行过程可以分为以下几个步骤:

  1. 执行同步代码(宏任务)。
  2. 当执行栈为空时,查询是否有异步任务需要执行。
  3. 执行微任务队列中的任务。
  4. 如果有需要,进行页面渲染。
  5. 执行宏任务队列中的任务,进入下一轮事件循环。

在同一次中,微任务一定优先于宏任务(X)

这句话是错的,第一步执行同步代码其实是宏任务

示例:Promise和setTimeout的执行顺序

继续通过一个具体的例子来进一步理解事件循环的执行顺序:

 1console.log(1);
 2
 3setTimeout(() => {
 4  console.log(2);
 5  new Promise((resolve) => {
 6    console.log(4);
 7    resolve()
 8    setTimeout(() => {
 9      console.log(6);
10    })
11  }).then(() => {
12    console.log(5);
13  })
14}, 1000)
15
16console.log(3);
17

输出结果将是:

1
3
2
4
5
6

这是因为微任务(Promise)会在宏任务(setTimeout)之前执行。微任务和宏任务都是队列这种数据结构,遵循先进先出。

异步编程的应用

理解了JavaScript的事件循环机制和异步处理后,我们可以更好地应用这些知识来处理实际的异步场景。例如,通过使用Promise来处理异步操作,编写更清晰、可维护的代码。

async-await

为了更方便地处理异步操作,使异步代码看起来更像同步代码,更容易理解和维护, asyncawait 是 ECMAScript 在2017(ES8)引入的特性。通常情况下, asyncawait 是一起使用的。它们的原理是当读取这行代码时立即执行 await 这行代码,并把它下一行的代码放到微任务当中。

示例:

 1console.log(1);
 2async function fn1() {
 3    await fn2()
 4    await fn3()
 5    console.log('fn1 end');
 6}
 7fn1()
 8async function fn2() {
 9  console.log('fn2 end');
10}
11async function fn3() {
12  console.log('fn3 end');
13}
14console.log(2);
15setTimeout(() => {
16  new Promise((resolve) => {
17    console.log('setTimeout');
18    resolve()
19  })
20  .then(() => {
21    console.log('then');
22  })
23  .then(() => {
24    setTimeout(() => {
25      console.log('then2 end');
26    })
27  })
28  console.log('setTimeout end');
29})
30

输出结果将是:

 11
 2fn2 end
 32
 4fn3 end
 5fn1 end
 6setTimeout
 7setTimeout end
 8then
 9then2 end
10

结论

JavaScript事件循环是保证异步编程顺利执行的关键机制。通过理解进程、线程、浏览器多线程、JavaScript的单线程性质以及事件循环的执行过程,我们可以更好地编写高效、响应迅速的JavaScript代码。异步编程、宏任务和微任务的概念是每个JavaScript开发人员都应该深入理解的内容,因为它们对于构建现代、高性能的Web应用至关重要。