前端开发学习笔记(八) : JavaScript的异步事件和Promise
JavaScript的异步事件和Promise

JavaScript 的异步编程是其核心特性之一,它允许在不阻塞主线程的情况下处理耗时操作(如网络请求、文件读写等)。以下从异步事件机制、Promise 的设计原理及其应用进行系统讲解,并结合事件循环深入分析。

一、JavaScript 异步事件机制

1. 单线程与异步的必要性

JavaScript 是单线程语言,意味着同一时间只能执行一个任务。若所有操作同步执行,耗时任务(如网络请求)会阻塞后续代码,导致页面卡顿。异步编程通过将任务交给其他线程(如浏览器 Web APIs)处理,主线程继续执行后续代码,待异步任务完成后再通过回调函数处理结果。

2. 事件循环(Event Loop)

事件循环是 JavaScript 处理异步任务的核心机制,由以下部分组成:

  • 调用栈(Call Stack):执行同步代码,遵循 LIFO(后进先出)原则。
  • 任务队列(Task Queue):分为宏任务队列(macro-task queue)和微任务队列(micro-task queue)。
    • 宏任务setTimeoutsetInterval、I/O 操作、UI 渲染等。
    • 微任务Promise.then()MutationObserverqueueMicrotask 等。
  • 事件循环流程
    1. 执行调用栈中的同步代码。
    2. 遇到异步任务,将其交给 Web APIs 处理,完成后将回调推入任务队列。
    3. 当调用栈为空时,优先清空微任务队列中的所有任务。
    4. 执行一个宏任务,重复步骤 3。
console.log("1"); // 同步任务

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => console.log("3")); // 微任务

console.log("4"); // 同步任务

// 输出顺序:1 → 4 → 3 → 2

二、Promise 的设计与使用

1. 回调地狱与 Promise 的诞生

传统回调函数嵌套(Callback Hell)导致代码难以维护:

getData(function (a) {
  getMoreData(a, function (b) {
    getMoreData(b, function (c) {
      // ...
    });
  });
});

Promise 通过链式调用(Chaining)和统一错误处理解决了这一问题。

2. Promise 的状态与生命周期

  • 三种状态
    • pending:初始状态,未完成也未拒绝。
    • fulfilled:操作成功完成,调用 resolve()
    • rejected:操作失败,调用 reject()
  • 状态不可逆:一旦状态变为 fulfilledrejected,不可再改变。

3. 创建 Promise

使用 new Promise() 构造函数,传入执行器函数(executor):

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5;
    success ? resolve("Success!") : reject("Error!");
  }, 1000);
});

4. 链式调用与错误处理

  • .then(onFulfilled, onRejected):处理成功或失败状态,返回新 Promise。
  • .catch(onRejected):捕获链中任何错误。
  • .finally():无论成功与否都执行,常用于清理操作。
fetchData()
  .then((data) => processData(data))
  .then((result) => displayResult(result))
  .catch((error) => console.error("Error:", error))
  .finally(() => stopLoading());

5. 静态方法

  • Promise.all([p1, p2]):所有 Promise 成功时返回结果数组,任一失败立即拒绝。
  • Promise.race([p1, p2]):返回最先完成的 Promise 的结果。
  • Promise.allSettled([p1, p2]):等待所有 Promise 完成,返回状态和结果数组。
  • Promise.resolve()/reject():快速创建已解决/拒绝的 Promise。

三、Promise 与异步函数的结合

1. Async/Await 语法糖

async/await 是基于 Promise 的语法糖,使异步代码更像同步写法:

  • async 函数始终返回 Promise。
  • await 暂停函数执行,直到 Promise 完成。
async function fetchData() {
  try {
    const response = await fetch("api/data");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
  }
}

2. 错误处理策略

  • 使用 try/catch 包裹 await 表达式。
  • 在 Promise 链中合理使用 .catch()

四、高级应用与注意事项

1. 避免常见陷阱

  • 未捕获的拒绝:始终添加 .catch() 处理错误。
  • 冗余嵌套:避免在 .then() 内部返回新 Promise,应直接链式调用。
  • 并行与顺序执行
    • 并行:Promise.all([task1(), task2()])
    • 顺序:task1().then(task2)

2. 自定义异步流程控制

通过封装 Promise 实现复杂逻辑,如重试机制、超时处理等。

function retry(fn, retries = 3, delay = 1000) {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        retries > 0
          ? setTimeout(
              () =>
                retry(fn, retries - 1, delay)
                  .then(resolve)
                  .catch(reject),
              delay
            )
          : reject(error);
      });
  });
}

五、总结

  • 异步机制:事件循环通过任务队列管理异步回调,微任务优先于宏任务执行。
  • Promise 核心:状态不可逆、链式调用、错误冒泡。
  • 最佳实践:优先使用 async/await 提高可读性,合理处理错误,善用静态方法处理并发任务。

理解这些概念后,可以编写高效、健壮的异步 JavaScript 代码,避免回调地狱和运行时错误。

手写 Promise.all 和 Promise.race

1. 手写 Promise.all

功能:当所有输入的 Promise 都成功时返回结果数组;当任意一个 Promise 失败时立即失败。

实现步骤

  1. 返回新 Promise。
  2. 将可迭代对象转换为数组。
  3. 处理空数组情况,直接 resolve。
  4. 遍历数组,用 Promise.resolve 包装每个元素以确保处理 Promise。
  5. 跟踪完成数量和结果数组,确保顺序。
  6. 任一 Promise 失败时立即 reject。

代码实现

function myPromiseAll(iterable) {
  const promises = Array.from(iterable);
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      resolve([]);
      return;
    }
    const results = new Array(promises.length);
    let remaining = promises.length;
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value;
          remaining--;
          if (remaining === 0) resolve(results);
        })
        .catch(reject);
    });
  });
}

难点分析

  • 顺序保证:通过索引存储结果,确保顺序与输入一致。
  • 错误处理:一旦有 Promise 被 reject,立即调用外层 reject,后续处理将被忽略。

2. 手写 Promise.race

功能:返回第一个解决(resolve 或 reject)的 Promise 的结果。

实现步骤

  1. 返回新 Promise。
  2. 遍历数组,用 Promise.resolve 包装每个元素。
  3. 每个 Promise 解决时立即调用外层 resolve 或 reject。

代码实现

function myPromiseRace(iterable) {
  const promises = Array.from(iterable);
  return new Promise((resolve, reject) => {
    promises.forEach((promise) => {
      Promise.resolve(promise).then(resolve).catch(reject);
    });
  });
}

难点分析

  • 快速响应:第一个解决的 Promise 触发外层 Promise 状态变更,后续结果被忽略。
  • 非 Promise 处理Promise.resolve 确保所有元素转为 Promise,普通值会立即 resolve。

Promise.resolve 和 Promise.reject 详解

1. Promise.resolve

功能:将值转换为 Promise 对象。处理规则:

  • 若值为 Promise,直接返回。
  • 若值为 thenable 对象(有 then 方法),转换为 Promise 并立即执行 then
  • 否则,返回 resolved 的 Promise。

示例

// 普通值
Promise.resolve(42).then(console.log); // 42

// Promise 对象
const p = Promise.resolve("hello");
Promise.resolve(p) === p; // true

// thenable 对象
const thenable = {
  then(resolve) {
    resolve("done");
  },
};
Promise.resolve(thenable).then(console.log); // 'done'

// 嵌套 thenable
const nestedThenable = {
  then(resolve) {
    resolve({ then: (r) => r(100) });
  },
};
Promise.resolve(nestedThenable).then(console.log); // 100

注意事项

  • 处理 thenable 时可能递归解析,直到非 thenable 值。
  • 若 thenable 的 then 方法抛出错误,返回的 Promise 会 reject。

2. Promise.reject

功能:返回一个立即 reject 的 Promise,参数作为拒绝原因。

示例

// 拒绝原因可以是任意类型
Promise.reject(new Error("失败")).catch((err) => console.log(err.message)); // '失败'
Promise.reject("直接拒绝").catch(console.log); // '直接拒绝'

// 即使参数是 Promise,也会作为原因传递
const p = Promise.resolve("test");
Promise.reject(p).catch(console.log); // 打印 Promise 对象,而非 'test'

注意事项

  • 参数不会被解析,直接作为 reject 的原因。
  • Promise.resolve 不同,不会处理 thenable 或 Promise。

最后修改于 2025-03-22