前端开发学习笔记(八) : 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)。
- 宏任务:
setTimeout
、setInterval
、I/O 操作、UI 渲染等。 - 微任务:
Promise.then()
、MutationObserver
、queueMicrotask
等。
- 宏任务:
- 事件循环流程:
- 执行调用栈中的同步代码。
- 遇到异步任务,将其交给 Web APIs 处理,完成后将回调推入任务队列。
- 当调用栈为空时,优先清空微任务队列中的所有任务。
- 执行一个宏任务,重复步骤 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()
。
- 状态不可逆:一旦状态变为
fulfilled
或rejected
,不可再改变。
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 失败时立即失败。
实现步骤:
- 返回新 Promise。
- 将可迭代对象转换为数组。
- 处理空数组情况,直接 resolve。
- 遍历数组,用
Promise.resolve
包装每个元素以确保处理 Promise。 - 跟踪完成数量和结果数组,确保顺序。
- 任一 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 的结果。
实现步骤:
- 返回新 Promise。
- 遍历数组,用
Promise.resolve
包装每个元素。 - 每个 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