潇湘一夜雨

JavaScript 事件循环,Promise 与 async/await

一、浏览器

1、浏览器的多进程

现代浏览器采用多进程+多线程架构,以提升性能、稳定性与安全性
现代浏览器的常见进程包括:1个浏览器主进程,1个GPU进程,1个网络进程,多个渲染进程,和多个插件进程

  • 浏览器主进程(Browser Process): 负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能

  • GPU进程(GPU Process): 负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程

  • 网络进程(Network Process): 负责发起和接受网络请求,以前是作为模块运行在浏览器进程一时在面的,后面才独立出来,成为一个单独的进程

  • 插件进程(Plugin Process): 主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响

  • 渲染进程(Renderer Process): 负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎Blink和JS引擎V8都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程

  • 其他辅助进程(Renderer Process):

    • 管理IndexedDB、LocalStorage。
    • Service Worker进程​:处理离线缓存与后台同步

2、浏览器的多线程

我们平时看到的浏览器呈现页面过程中,大部分工作都是在渲染进程中完成,主要有以下几个线程:GUI渲染线程、JS引擎线程、事件触发线程、计时器线程和异步http请求线程

  • GUI渲染线程: 负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行,最终将网页内容渲染到屏幕上。

  • JS引擎线程(主线程): 一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS。GUI渲染进程不能同时执行,只能一个一个的执行,如果JS执行过长就会导致阻塞掉帧

  • 定时器线程: 指setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作

  • 异步http请求线程: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等JS引擎空闲执行

  • 事件触发线程: 主要用来控制事件循环,比如JS执行遇到计时器,AJAX异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等JS引擎处理

  • ​合成线程: 分层(Layer)处理动画与滚动,移交GPU进程渲染。实现GPU加速,减少重排

  • ​​Web Worker线程: 执行CPU密集型任务(如数据分析),通过postMessage与主线程通信(无DOM访问权限,独立内存空间)

  • ​​IO线程: 用来和其他进程进行通信。例如: 网络进程下载完成以后就会将信息发送给 IO 线程,通过IPC 通信。当用户点击页面按钮,浏览器主线程就会把这个信息通过 IPC 发送给 IO 线程。IO 线程会将这个时间包装成任务添加到消息队列里面,供事件循环机制不断处理。
    在这里插入图片描述

二、为什么JavaScript是单线程?

JavaScript采用单线程设计主要是为了避免多线程操作DOM的复杂性、简化语言实现,并通过事件循环机制实现异步非阻塞操作。

1、避免DOM操作冲突‌

JavaScript 最初设计用于浏览器环境,主要操作 DOM 和响应用户交互。若允许多线程同时操作 DOM,可能导致竞态条件(如一个线程删除节点,另一个线程修改该节点),引发不可预测的结果

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
虽然JavaScript本身是单线程的,但是浏览器是多线程的,通过浏览器提供的WebAPIs实现异步功能

2、简化语言实现‌

多线程需处理线程同步、死锁等问题,增加复杂度。单线程模型降低了引擎实现难度,更适合早期浏览器轻量级脚本的定位。‌‌

三、浏览器的事件循环(Event Loop)

JavaScript 是单线程的,这意味着它只有一个主线程(Main Thread)负责执行代码,一次只能执行一个任务。为了处理耗时的操作(如网络请求、定时器、用户交互等)而避免阻塞主线程,JavaScript 引入了事件循环 (Event Loop) 机制,并区分了宏任务 (Macrotasks) 微任务 (Microtasks)。它通过任务队列和循环调度机制实现非阻塞的异步执行。

一、事件循环的核心角色

1、调用栈(Call Stack)​​

  • 定义: 调用栈也叫执行栈,调用栈是主线程内部的数据结构,用于管理函数调用顺序(后进先出),主线程通过调用栈执行同步代码。

    主线程(JS引擎线程)是JavaScript代码执行的唯一线程,负责执行所有同步代码及异步任务的回调函数。同步代码的执行过程完全由调用栈控制,主线程是调用栈的执行者。

    主线程是执行者​: 唯一执行JS代码的线程,通过调用栈管理函数调用顺序
    调用栈是调度器​: 记录函数执行状态,确保同步代码顺序执行;空闲时由事件循环注入异步任务回调

  • 职责​: 按顺序执行同步代码,采用后进先出(LIFO)结构管理函数调用。函数调用时入栈,执行完毕出栈

  • 关键特性​: 同步代码执行时会阻塞后续任务,仅当调用栈清空时事件循环才会处理队列任务

2、任务队列(Task Queues)​​

  • ​宏任务队列(Macrotask Queue)​​:
    存放 setTimeout、setInterval、I/O 操作、DOM 事件等回调。每次事件循环仅取一个宏任务执行

  • ​微任务队列(Microtask Queue)​​:
    存放 Promise.then、MutationObserver、queueMicrotask 等回调。优先级高于宏任务,需在当前事件循环中全部清空后才会执行下一个宏任务

3、Web APIs / 宿主环境​

  • ​职责​:提供异步能力(如浏览器中的定时器线程、I/O 线程)。当调用栈遇到异步操作时,将其交给宿主环境处理,完成后将回调推入对应队列

  • ​示例​:setTimeout 由定时器线程计时,到期后回调推入宏任务队列;Promise.then 由引擎直接推入微任务队列

4、​事件循环调度器(Event Loop Scheduler)​​

  • ​职责​:持续检查调用栈和队列状态,按规则调度任务执行。核心流程为:同步代码 → 清空微任务 → 执行一个宏任务 → 循环

二、初始宏任务

1、初始宏任务这个概念更多是用来描述一个应用或网页加载初期,最先被执行的一批宏任务。

2、当浏览器加载一个 HTML 页面并解析时,其中的<script> 标签内的JavaScript代码会被视为初始宏任务,这个初始宏任务会直接压入调用栈按顺序执行。

3、如果HTML页面存在多个<script>标签,存在于HTML文档的不同位置(例如,分别在<head>和<body>中),它们将按照<script>标签在HTML文档中出现的顺序依次执行。

4、在HTML页面中,每个<script>标签中的代码执行被视为一个初始宏任务。因此,如果页面中有多个<script>标签,默认情况下每个<script>标签的代码执行都是独立的宏任务。

初始宏任务不会加入宏任务队列,而是直接压入调用栈按顺序执行

注意: 初始宏任务也是宏任务,只是初始宏任务JS主线程直接执行,不入宏任务队列

示例:

//第一个初始宏任务
<script>
    console.log("a")
</script>
//第二个初始宏任务
<script>
    console.log("b")
</script>

三、宏任务(Macrotask)

  • 定义: 宏任务代表较大的、独立的异步任务,通常由​宿主环境(如浏览器)发起的任务,包括用户交互、定时器、网络请求等需要异步处理的操作。它们被添加到宏任务队列中,由事件循环按顺序执行。
  • 常见宏任务:
    • setTimeout / setInterval (定时器回调)
    • I/O 操作(如文件读写、网络请求)
    • DOM事件回调(点击、滚动等)
    • UI 渲染(浏览器重绘/回流)
    • script 标签中的整体代码(初始宏任务)
    • setImmediate(Node.js 特有)
  • 初始宏任务与宏任务的区别
    初始宏任务是网页加载初期被加载的<script>代码,不入宏任务队列,执行压入调用栈中执行,宏任务是在执行初始宏任务的时候,初始宏任务中存在异步代码,并交于WebApi处理,并被推入宏任务队列。

四、微任务(Microtask)

  • 定义: 微任务是比宏任务更小的异步任务,通常用于​高优先级回调
  • 常见微任务:
    • Promise.then / Promise.catch / Promise.finally (Promise 回调)
    • MutationObserver(监听 DOM 变化)
    • process.nextTick(Node.js 特有,优先级高于其他微任务)
    • queueMicrotask(浏览器原生微任务 API)

五、代码执行顺序

1、执行当前宏任务中的同步代码。
2、检查微任务队列并执行全部微任务(若执行中产生新微任务,继续加入当前队列执行)
3(可选)浏览器可能进行 DOM 渲染(样式计算、布局、绘制等)
4、从宏任务队列取下一个任务,重复上述流程

​同步代码 → 微任务 → 宏任务 → UI 渲染 → 下一个宏任务...​

示例:

<script>
	console.log("Script 1 - 同步代码"); //同步代码,立即执行
	setTimeout(() => console.log("setTimeout 1"), 0);  //异步代码。交由WebApi处理,回调会被加入到宏任务队列
	Promise.resolve().then(() => console.log("Promise.then 1")); //Promise.resolve().then的回调属于异步代码。交由WebApi处理,回调会被加入到微任务队列
	console.log("第一个script结束"); //同步代码,立即执行
</script>

<script>
	console.log("Script 2 - 同步代码"); //同步代码,立即执行
	setTimeout(() => console.log("setTimeout 2"), 0); //异步代码。交由WebApi处理,回调会被加入到宏任务队列
	Promise.resolve().then(() => console.log("Promise.then 2")); //Promise.resolve().then属于异步代码。交由WebApi处理,回调会被加入到微任务队列
	console.log("第二个script结束"); //同步代码,立即执行
</script>
/*
以上HTML页面存在2个<script>,所以这个2个<script>会作为2个初始宏任务,会按照JS文档的顺序依次执行
初始宏任务中的同步代码会立即推入调用栈(Call Stack)执行
初始宏任务中的异步代码会根据任务类型将它的回调推入到对应的宏任务队列或微任务队列中

执行顺序如下:

第一个 <script> 标签:
1、当执行到:console.log("Script 1 - 同步代码")是同步代码,会立即压入调用栈执行。输出:Script 1 - 同步代码
2、当执行到:setTimeout(() => console.log("setTimeout 1"), 0)的时候,因为setTimeout()的回调属于异步微任务,所以会将代码交由浏览器的Web Api处理,然后浏览器内核的定时器线程(非JS主线程)负责计时,计时期间,回调函数() => console.log("setTimeout 1")和计时参数会被封装为任务,存储到延迟队列​(delayed_incoming_queue)中。计时结束后,​定时器线程通知事件触发线程,事件触发线程将回调函数从延迟队列移入宏任务队列想队尾。
3、当执行到:Promise.resolve().then(() => console.log("Promise.then 1"))的时候,.then的回调属于异步微任务,它的回调() => console.log("Promise.then 1")会被JavaScript主线程推入到微任务队列(先进先出(FIFO))。
4、当执行到:console.log("第一个script结束")是同步代码,会立即压入调用栈执行。输出:第一个script结束

	此时第一个<script>中的代码就已经执行完毕,即第一个初始宏任务执行完毕。
	当第一个初始宏任务执行完毕后,JS主线程会检查微任务队列是否为空,若队列非空,则取出最老的微任务​推入调用栈执行,直至微任务队列为空。此时微任务队列中有一条任务。JS主线程取出这条任务进行执行。
5、() => console.log("Promise.then 1")会从微任务队列取出,被压入调用栈进行执行。输出:Promise.then 1

	当微任务队列中的任务执行完毕,微任务被清空后会执行下一个宏任务(文档优先)所以HTML页面的第二个<script>标签会被执行(<script>属于初始宏任务,所以它不会加入到宏任务队列的,而是按照<script>标签在HTML文档中出现的顺序依次执行)

第二个 <script> 标签:
6、当执行到:console.log("Script 2 - 同步代码")是同步代码,会立即压入调用栈执行。输出:Script 2 - 同步代码
7、当执行到:setTimeout(() => console.log("setTimeout 2"), 0)的时候,发现setTimeout()的回调属于异步代码,会将代码交由浏览器的Web Api处理,然后浏览器内核的定时器线程(非JS主线程)负责计时,计时期间,回调函数() => console.log("setTimeout 2")和计时参数会被封装为任务,存储到延迟队列​(delayed_incoming_queue)中。计时结束后,​定时器线程通知事件触发线程,事件触发线程将回调函数从延迟队列移入宏任务队列想队尾。
8、当执行到:Promise.resolve().then(() => console.log("Promise.then 2"))的时候,因为Promise.resolve().then的的回调属于异步微任务,所以() => console.log("Promise.then 2")会被JavaScript主线程推入到微任务队列(先进先出(FIFO))。
9、当执行到:console.log("第二个script结束")是同步代码,会立即压入调用栈执行。输出:第二个script结束

	此时第二个<script>中的代码就已经执行完毕,即第二个初始宏任务执行完毕。
	当第二个初始宏任务执行完毕后,JS主线程会检查微任务队列是否为空,若队列非空,则取出最老的微任务​推入调用栈执行,直至微任务队列为空。此时微任务队列中有一条任务。JS主线程取出这条任务进行执行。此时微任务队列中有一条任务。JS主线程取出这条任务

10、() => console.log("Promise.then 2")会从微任务队列取出,被压入调用栈进行执行。输出:Promise.then 2

	当微任务队列中的任务执行完毕,微任务被清空后会执行下一个宏任务(文档优先),此时HTML文档中的初始宏任务已经执行完毕了,所以会从宏任务队列中取出一条宏任务执行(先进先出(FIFO))。(此时宏任务队列中存在2条宏任务)

11、() => console.log("setTimeout 1")会从宏任务队列取出,被压入调用栈进行执行。输出:setTimeout 1

	() => console.log("setTimeout 1")这个宏任务执行完毕后JS主线程会检查微任务队列是否为空,此时微任务队列为空,没有微任务可以执行,所以就从宏任务队列取出下一条宏任务执行,此时宏任务队列还剩下一条宏任务

12、() => console.log("setTimeout 2")会从宏任务队列取出,被压入调用栈进行执行。输出:setTimeout 2

	() => console.log("setTimeout 2")这个宏任务执行完毕后会检查微任务队列,此时微任务队列为空,没有微任务可以执行,此时宏任务队列也为空了。至此所有JS代码执行完毕。
*/

输出顺序如下:

Script 1 - 同步代码
第一个script结束
Promise.then 1
Script 2 - 同步代码
第二个script结束
Promise.then 2
setTimeout 1
setTimeout 2

异步代码:微任务的优先级高于宏任务

setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
// 输出顺序:微任务 → 宏任务

await 之后的代码会被包装成微任务,但 await 之前的代码是同步执行的:

async function foo() {
  console.log('await之前'); // 此代码位于await之前,属于同步代码,立即执行。
  await Promise.resolve(); 
  console.log('await之后'); // 此行代码位于await之后,会被包装成微任务,加入微任务队列。
}
console.log('同步1');
foo();
console.log('同步2');

输出顺序为:

同步1
await之前
同步2
await之后

四、什么是回调地狱

在JavaScript中,“地狱回调”通常指的是“回调地狱(Callback Hell)”,这是一种编程现象,主要发生在异步编程中。当有多个嵌套的异步操作,每个操作都在前一个操作的成功回调函数内部启动时,代码会形成层层嵌套的结构,使得代码难以阅读和维护。这种嵌套层次过深的情况就被形象地称为“回调地狱”。

回调地狱的主要特点包括:

  1. 深层嵌套结构​
    多个回调函数逐层嵌套,每个异步操作完成后,下一个异步操作被嵌套在前一个的回调函数中。
  2. 重复错误处理​
    每个嵌套层需单独处理错误,导致冗余代码。
  3. 可读性与维护性差​
    逻辑分散在嵌套中,难以追踪执行流程,调试和修改这样的代码非常不便。

例如:

asyncOperation1(function(result1) {
    asyncOperation2(result1, function(result2) {
        asyncOperation3(result2, function(result3) {
            console.log(result3);
        });
    });
});

为了解决这个问题,JavaScript社区提出了几种解决方案:

1、使用Promise:通过.then()方法链式调用,可以有效地减少嵌套层级,使代码更加扁平化和易读。

asyncOperation1()
    .then(result1 => asyncOperation2(result1))
    .then(result2 => asyncOperation3(result2))
    .then(result3 => console.log(result3))
    .catch(error => console.error(error));

2、async/await:这是ES2017引入的一个特性,允许你以同步的方式编写异步代码,极大地提高了代码的可读性和简洁性。

async function executeOperations() {
    try {
        const result1 = await asyncOperation1();
        const result2 = await asyncOperation2(result1);
        const result3 = await asyncOperation3(result2);
        console.log(result3);
    } catch (error) {
        console.error(error);
    }
}

五、Promise

1、定义与本质

Promise 是 JavaScript 中处理异步编程的核心解决方案,由 ES6 规范正式引入。它通过状态管理、链式调用等机制,解决了传统回调函数嵌套导致的“回调地狱”问题,使异步逻辑更易读、可维护。
其本质就是一个异步容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

Promise 是一个代理对象,代表一个未来才会完成的异步操作(如网络请求、文件读取)。它保存异步操作的最终结果(成功值或失败原因),并通过状态隔离外部依赖

2、它有三种状态

  1. pending:(进行中):初始状态,既不是成功,也不是失败
  2. fulfilled:(已成功):操作成功完成(调用了 resolve)
  3. rejected:(已失败):操作失败(调用了 reject)

一旦 Promise 状态从 pending 变为 fulfilled 或者 rejected,这个 Promise 的状态就不能再改变,并且相应的结果值也会被固定下来。

3、语法与案例

标准语法

<script>
    const promise = new Promise((resolve, reject) => {
        // 执行器函数(executor)
        if (true) {
            resolve(value); // 将 Promise 状态变为 fulfilled,并传递值
        } else {
            reject(reason); // 将 Promise 状态变为 rejected,并传递原因
        }
    });

    //then() 可以接收两个参数:成功回调和失败回调(也可以省略,推荐用 .catch)
    promise.then(
        (value) => { /* 处理成功结果 */ },
        (error) => { /* 可选:处理失败 */ } //
    )

    //以上promise.then的第二个参数是处理失败,推荐用.catch
    //.catch(onRejected)专门用于捕获错误,等价于 .then(null, onRejected)
    promise.then(value => { /* 处理成功结果 */ }).catch(error => {/* 可选:处理失败 */ })
</script>

推荐使用链式语法

<script>
    new Promise((resolve, reject) => {
        if (2 < 1) {
            resolve("成功") //同步任务
        } else {
            reject("失败") //同步任务
        }
    })
    .then(result => { console.log(result) })  //异步:成功回调,处理resolve()的返回值。状态为 ​fulfilled
    .catch(error => { console.log(error) })   //异步:失败回调,处理reject()的返回值。状态为 ​rejected
    .finally(() => { /* 清理逻辑(如关闭资源) */ }) // 异步:回调是一个无参数的函数,无论Promise是resolved还是rejected都会执行。 
</script>

案例

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
    function GetDate() {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: 'https://ipapi.co/json/',          // 请求地址 
                type: 'GET',                            // 请求类型为GET 
                success: function (response) {          // 成功回调
                    resolve(response)                   // 处理返回数据
                },
                error: function (xhr, status, error) {  // 失败回调
                    reject(error);                      // 输出错误信息
                }
            });
        })
    }
    GetDate()
    .then(value => { console.log(value) })
    .catch(error => console.log(error))
    .finally(()=>{/*这里可以做点收尾工作,无论请求失败或者成功这里都会被执行到*/ });
</script>

4、resolve 和 reject 的核心作用

函数 ​状态变更 ​传递值 ​触发回调
resolve pending → fulfilled 成功的结果(称为 value) 触发 .then() 的第一个回调
reject pending → rejected 失败的原因(称为 reason) 触发 .then() 的第二个回调或 .catch()

关键特性​:
​状态唯一性​: Promise 状态一旦变为 fulfilled 或 rejected,便不可逆转
​传递值类型​:
resolve 可传递任意类型(值、对象、另一个 Promise、thenable 对象)。
reject 通常传递错误对象(如 new Error())或字符串

5、代码详解

<script>
    console.log(1); // 👉 同步任务
    new Promise((resolve, reject) => {
        console.log(3);         // 👉 同步任务
        resolve(1000);			// 👉 同步任务(修改 Promise 状态,从pending修改为fulfilled)
        console.log(4);			// 👉 同步任务
    }).then(value => {
        console.log('结果是:', value); // 异步微任务
    });
    console.log(2); // 👉 同步任务
</script>

输出顺序:

1
3
4
2
结果是: 1000

✅一、执行步骤​:

  • 1、执行同步代码:console.log(1) :输出:1

  • 2、同步执行 Promise 构造函数

    • console.log(3) 立即执行,输出 3
    • resolve(1000) 同步执行,将 Promise 状态改为 ​fulfilled​(已完成),并传递值 1000。注意:此时 then 回调尚未触发,而是被放入微任务队列
    • console.log(4) 继续同步执行,输出 4
  • 3、执行同步代码:console.log(2) :输出:2

  • 4、​同步代码执行完毕

    • 此时主线程(宏任务)结束,开始检查微任务队列
  • 5、执行微任务队列中的回调

    • 微任务队列中存在 then 回调函数 value =>
    • 执行该回调,输出 结果是: 1000

✅二、关键机制说明

  1. Promise 构造函数的同步性
    new Promise() 中的执行器函数是同步执行的(即传入构造函数的 executor 函数),但 ​then() 的回调是异步的,属于微任务​。
    因此 console.log(3/4) 和 resolve() 会立即执行
  2. resolve() 的异步触发作用
    resolve() 会修改 Promise 状态,但 ​then 回调不会立即执行**,而是被推入 ​微任务队列,等待当前宏任务(同步代码)结束后才执行
  3. 事件循环优先级
    JavaScript 事件循环按以下顺序处理任务:
    同步代码(宏任务) → 微任务队列(如 Promise 回调) → 宏任务队列(如 setTimeout)
    本例中无宏任务,因此微任务在同步代码结束后立即执行。

✅三、常见误解澄清

  • resolve() 不是异步函数​: 它同步修改 Promise 状态,但触发 then 回调是异步的
  • 微任务队列的优先级​: 即使 resolve() 后紧跟着同步代码(如 console.log(4)),then 回调仍需等待同步代码全部完成才会执行

6、注意事项

1. 异步执行​:
Promise 的执行器(executor)是同步执行的,但then/catch/finally 的回调是异步的

2. ​错误穿透​:
then 中未定义错误回调,错误会向下传递至最近的 catch

3. ​返回值的处理​:
then 回调中返回普通值会包装为 fulfilledPromise;抛出错误会触发 rejected
在这里插入图片描述

7、.then()

.then() 接收两个可选的回调函数参数onFulfilledonRejected
.then() ​总是返回一个新的 Promise 对象,这是链式调用的基础。
.then() 的回调是微任务,在当前同步代码执行完毕后执行、多个 .then() 按注册顺序依次执行

1、 基础语法

promise.then(
  onFulfilled, // 成功回调(可选)
  onRejected   // 失败回调(可选)(推荐使用.catch替代)
);
// 或者
promise.then(onFulfilled).catch(onRejected); // 更推荐的写法

案例:
promise.then(result=>console.log(result)).catch(error=>console.log(error))

onFulfilled​:当 Promise 状态变为 fulfilled 时触发,接收成功值作为参数
onRejected​:当 Promise 状态变为 rejected 时触发,接收错误原因作为参数(推荐使用.catch替代)

8、.finally()

  • finally无论成败均执行,适合清理资源(如关闭连接),但不影响错误传递
    let dbConnection;
    openDatabase()  //返回一个Promise
     	.then(conn => { dbConnection = conn; })
    	.catch(console.error)
     	.finally(() => dbConnection?.close()); // 确保资源释放
    

9、.catch()

Promisecatch() 方法是处理异步操作错误的核心机制,其本质是 .then(null, onRejected) 的语法糖,专用于捕获 Promise 链中的拒绝rejected状态。

链式调用中,.catch() 可以捕获整个链中的错误

1、触发进入catch的4种情况

  1. Promise显式拒绝(Reject):
    • 当Promise执行器(executor)中调用reject()时:
      new Promise((resolve, reject) => {
      	reject(new Error("操作失败")); // 显式拒绝
      }).catch(error => console.log(error.message)); // 输出"操作失败"
      
  2. 同步/异步代码抛出异常
    • ​同步错误​: 执行器或then回调中抛出异常(如throw或未定义变量)
      // 注意:如果用reject("同步错误"));如果不用reject则需要用throw来抛出new Error("同步错误")
      new Promise(() => { throw new Error("同步错误"); }).catch(console.log); 
      
    • 异步错误​: setTimeout等宏任务中的错误需通过Promise封装捕获,否则catch无效
      // 错误示例:catch无法捕获
      new Promise(() => {
      	setTimeout(() => { throw new Error("异步错误"); }, 100);
      }).catch(console.log); // ❌ 无法捕获
      
      // 正确做法:封装为Promise
      new Promise((resolve, reject) => {
      	setTimeout(() => reject("异步错误"), 100);
      }).catch(console.log); // ✅ 捕获
      
  3. Promise链中前置操作失败
    • 链中任一then回调返回被拒绝的Promise或抛出错误,均会被后续最近的catch捕获
      Promise.resolve()
      	.then(() => { throw new Error("链中错误"); }) // 抛出错误
      	.then(() => console.log("不会执行")) // 跳过
      	.catch(error => console.log(error.message)); // 捕获"链中错误"
      
  4. 聚合方法中的拒绝(如Promise.all)
    • Promise.all中任一Promise被拒绝,整个聚合结果立即进入catch
      Promise.all([
      	Promise.resolve(1),
      	Promise.reject("失败"),
      	Promise.resolve(3)
      ]).catch(error => console.log(error)); // 输出"失败"
      

2、未处理拒绝的全局捕获

  • 未被任何catch处理的拒绝会触发unhandledrejection事件
    window.addEventListener("unhandledrejection", event => {
    	console.error("未捕获的Promise错误:", event.reason);
    });
    

3、链式调用中catch的行为特点

  1. 错误冒泡与中断
    • 错误会沿Promise链向后传递,直到被最近的catch捕获
    • 捕获后,后续的then链仍可继续执行(除非再次抛出错误)
      Promise.reject("错误")
      .catch(() => console.log("处理完成")) // ✅ 捕获
      .then(() => console.log("继续执行")); // ✅ 输出"继续执行"
      
  2. 重新抛出错误(Rethrow)
    • 在catch中再次抛出错误,可传递给下一个错误处理器
      Promise.reject(new TypeError("类型错误"))
      .catch(error => {
      	if (error instanceof TypeError) 
      		throw error; // 重新抛出
      	else console.log("其他错误");})
      .catch(error => console.log("捕获重新抛出的错误")); // ✅
      

六、async/await

async/await实际上是建立在Promise之上的高级封装,让异步代码可以像同步代码一样书写。这大大提高了代码的可读性和可维护性。
async/await Promise 的语法糖
asyncJavaScript 中用于声明异步函数的关键字

1、async 关键字

用于声明一个函数为异步函数(async function)。该函数始终返回 Promise 对象,若函数内无 return 语句(或 return 无值),则默认返回 Promise.resolve(undefined),即解析值为 undefined 的 Promise。

特点

  • 若函数返回非 Promise 值(如 return 42),会自动包装为 return Promise.resolve(42)
  • 若抛出错误(如 throw new Error()),返回 return Promise.reject(error)
  • async函数没有返回值,则默认返回return Promise.resolve(undefined)

声明

  1. 声明异步函数:async 关键字将函数标记为异步函数,使其内部可以使用 await 表达式

    async function fetchData() {
    	const response = await fetch('https://api.example.com/data');
    	return response.json();
    }
    
  2. 确保函数返回 Promise:无论 async 函数内部返回什么值(包括非 Promise 值),它都会被自动包装为一个 Promise。

    //调用 foo() 时,会得到一个状态为 fulfilled 的 Promise,其值为 42。
    async function foo() {
    	return 42; // 自动包装为 Promise.resolve(42) 进行返回
    }
    
    async function boo(){
    	await 43;  // 等价于 await Promise.resolve(42)
    }
    
    //调用 bar() 时,会得到一个状态为 rejected 的 Promise,其拒绝原因为 Error("Oops!")。
    async function bar() {
    	throw new Error("Oops!"); // 自动包装为 Promise.reject(new Error("Oops!")) 进行返回
    }
    
    
    

2、await 关键字

1、核心机制:暂停执行

在JavaScript中,await 关键字的作用是在等待右侧的表达式返回的 Promise 解决(resolve) 之后,才会暂停 async 函数的执行

具体来说,当JavaScript引擎遇到 await 时,它会做以下两件事:

1、 立即执行右侧表达式

  • 当执行到 await 时,首先会立即计算其右侧的表达式(无论它是函数调用、变量还是其他表达式)
  • 如果右侧表达式返回一个 Promise,则进入等待状态
  • 如果返回非 Promise 值(如数字、字符串等),引擎会将其隐式转换为一个已解决的 Promise 如:Promise.resolve(value)
    async function someAsync(){ console.log(3); //相当于return Promise.resolve(undefined)}
    async function foo(){
    	console.log(1);
    	//这个someAsync是一个async函数(即:我们上面说的await右侧的表达式)
    	
    	//async函数即便没有显式返回值,依然会默认返回一个解析为 undefined 的 Promise 对象
    	//即:即便没有显示返回值,也会默认:return Promise.resolve(undefined)
    	
    	//当执行到await someAsync()这行代码的时候,someAsync()函数是同步立即执行
    	//直到等到someAsync()函数同步执行完毕return一个Promose对象:例如:return Promise.resolve(undefined)
    
    	//当await拿到这个Promise对象的时候会暂停foo()函数的后续执行。然后为这个Promise对象注册一个微任务回调
    	//然后将await someAsync()后面的代码添加到这个回调中。然后这个回调会加入微任务队列
    	//即:Promise.resoleve().then((value)=>{console.log(2)}); 
    	await someAsync(); //同步立即执行
    	console.log(2)     //异步微任务
    }	
    foo();
    

2、暂停执行 async 函数

  • 在右侧表达式执行完成后,如果结果是一个 Promise(无论是显式返回还是隐式转换),await 会立即暂停当前 async 函数的执行,并将控制权交还给调用栈(事件循环可以继续处理其他任务)。

  • 如果右侧表达式返回一个未完成的 Promiseasync 函数会一直暂停,直到该 Promise 状态变为 fulfilled 或 rejected

关键结论
await 不会在表达式执行前暂停,而是先执行右侧表达式,再根据结果决定是否暂停。

暂停发生在表达式执行后,且仅在需要等待 Promise 结果时发生(如果是非 Promise 值,暂停时间极短,因为会立即恢复)

​返回值处理​

  • Promise 解决(fulfilled),await 返回其结果值
  • Promise 拒绝(rejected),await 抛出异常,可通过 try/catch 捕获
  • ​非 Promise 值​:若右侧是非 Promise(如 await 123),引擎自动包装为 Promise.resolve(123),直接返回值本身

2、核心机制:恢复执行

当调用栈为空的时候(即:同步任务执行完毕),会优先执行微任务队列,从队列中逐条取出任务压入调用栈执行。当调用栈为空,且微任务队列为空的时候,会检查宏任务队列是否为空,如果不为空,则从宏任务队列逐步取出任务执行。

3、执行流程控制

在这里插入图片描述

4、​错误处理

使用 try/catch 捕获异常:

async function loadData() {
  try {
    const data = await fetchData();
  } catch (error) {
    console.error("请求失败:", error); // 捕获网络或解析错误
  }
}

5、连续 await 的性能问题

假设你需要从三个不同的 API 获取数据,这三个请求是相互独立的,它们的结果不依赖于彼此。
低效的写法(串行执行):

// 模拟 API 调用
function fetchUser() {
    return new Promise(resolve => setTimeout(() => resolve({ id: 1, name: "Alice" }), 200));
}
function fetchPosts() {
    return new Promise(resolve => setTimeout(() => resolve([{ id: 1, title: "Post 1" }]), 300));
}
function fetchSettings() {
    return new Promise(resolve => setTimeout(() => resolve({ theme: "dark" }), 150));
}

async function fetchDataSequentially() {
    console.log("开始获取数据...");

    const startTime = Date.now();

    // ❌ 串行执行:第二个请求必须等待第一个完成,第三个等待第二个...
    const user = await fetchUser();       // 假设耗时 200ms
    const posts = await fetchPosts();     // 假设耗时 300ms
    const settings = await fetchSettings(); // 假设耗时 150ms

    console.log("用户:", user);
    console.log("文章:", posts);
    console.log("设置:", settings);

    const endTime = Date.now();
    console.log(`总耗时: ${endTime - startTime}ms`); // 大约 650ms
}

问题: 这三个 fetch 操作是串行执行的。总时间是它们耗时的总和(200 + 300 + 150 = 650ms)。这浪费了网络和服务器的并行处理能力。

优化:并发执行
目标是让这三个独立的请求同时发起,然后等待它们全部完成。这可以通过 Promise.allPromise.allSettledPromise.race 来实现。

✅优化方案一:使用 Promise.all
Promise.all 接受一个 Promise 数组,返回一个新的 Promise。这个新的 Promise 在所有输入的 Promisefulfilledfulfilled,或者在任意一个输入的 Promise rejectedrejected

async function fetchDataConcurrently() {
    console.log("开始获取数据...");

    const startTime = Date.now();

    // ✅ 并发执行:同时发起所有请求
    // 1. 立即创建所有 Promise (发起请求)
    const userPromise = fetchUser();
    const postsPromise = fetchPosts();
    const settingsPromise = fetchSettings();

    // 2. 等待所有 Promise 完成
    // 这里的 await 会等待整个 Promise.all 完成
    const [user, posts, settings] = await Promise.all([
        userPromise,
        postsPromise,
        settingsPromise
    ]);

    console.log("用户:", user);
    console.log("文章:", posts);
    console.log("设置:", settings);

    const endTime = Date.now();
    console.log(`总耗时: ${endTime - startTime}ms`); // 大约 300ms (最长的那个)
}

优点:

性能最佳:总时间由耗时最长的那个请求决定(约 300ms),效率提升显著。
代码清晰。
缺点:

短路行为:如果数组中任何一个 PromiserejectedPromise.all 返回的 Promise 会立即 rejected,并且你得不到其他已经成功的结果。

✅优化方案二:使用 Promise.allSettled
Promise.allSettled 也接受一个 Promise 数组,返回一个新的 Promise。这个新的 Promise 在所有输入的 Promise 都已解决(无论是 fulfilled 还是 rejected)时才fulfilled

async function fetchDataWithAllSettled() {
    console.log("开始获取数据...");

    const startTime = Date.now();

    const promises = [
        fetchUser(),
        fetchPosts(),
        fetchSettings()
    ];

    // ✅ 并发执行,且等待所有结果(无论成功或失败)
    const results = await Promise.allSettled(promises);

    // 处理结果
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`请求 ${index + 1} 成功:`, result.value);
        } else {
            console.error(`请求 ${index + 1} 失败:`, result.reason);
        }
    });

    const endTime = Date.now();
    console.log(`总耗时: ${endTime - startTime}ms`); // 大约 300ms
}

优点:

健壮性好:即使某些请求失败,你仍然能得到所有请求的最终状态和结果(成功值或失败原因)。
适合那些“尽力而为”的场景,比如加载多个非关键的资源。
缺点:

你需要手动检查每个结果的 status

✅优化方案三:使用 Promise.race (特定场景)
Promise.race 返回一个 Promise,这个 Promise 在任意一个输入的 Promise 改变状态时,就立刻改变状态。

async function fetchDataWithRace() {
    // 场景:获取数据,但只想要最快的响应,或者设置超时
    const fetchPromise = fetchUser(); // 主请求
    const timeoutPromise = new Promise((_, reject) => 
        setTimeout(() => reject(new Error('请求超时')), 250)
    );

    try {
        // ✅ 等待第一个完成的 Promise
        const result = await Promise.race([fetchPromise, timeoutPromise]);
        console.log("获取成功:", result);
    } catch (error) {
        console.error("获取失败:", error.message); // 如果超时,会捕获到超时错误
    }
}

适用场景:

  • 超时控制: 非常常用。
  • 竞速: 获取多个来源的数据,只取最快的那个。

✅其他优化技巧
尽早发起请求:在 await 之前,尽早创建 Promise 对象(即发起请求),这样它们可以尽早开始执行。

async function example() {
    // 好:尽早发起
    const promiseA = apiCallA(); // 请求立即开始
    const promiseB = apiCallB(); // 请求立即开始
    
    // ... 可以做其他同步工作 ...
    
    const [resultA, resultB] = await Promise.all([promiseA, promiseB]);
}
  • 避免不必要的 await 如果你不需要立即使用结果,或者想并发执行,就不要 await
  • 使用 for await...of 处理异步可迭代对象(如流)
  • 错误处理: 使用 try...catch 包裹 await,或者使用 .catch()。对于 Promise.all,可以 try...catch 整个表达式,或者使用 Promise.allSettled 避免短路

总结

  • 避免独立的异步操作进行连续 await(串行)
  • 优先使用Promise.all进行并发,以获得最佳性能。
  • 当需要处理部分失败时,使用Promise.allSettled
  • 当需要超时或竞速时,使用 Promise.race

posted on 2025-11-26 16:41  潇湘一夜雨  阅读(0)  评论(0)    收藏  举报

导航