📌 1. JavaScript 为什么需要异步?

JavaScript 是 单线程(Single-threaded)语言,它的执行方式是逐行执行,但有时候代码的执行可能会遇到等待:

  • 你去点咖啡(等待制作)。
  • 你从服务器请求数据(等待网络)。
  • 你读取一个文件(等待磁盘)。

如果JavaScript只能同步执行,遇到等待时,整个程序都会卡住,无法继续执行。为了解决这个问题,JavaScript引入了异步编程

📌 2. 回调函数(Callback)—— 传统异步

最原始的异步方案是回调函数(callback),即当任务完成时,调用一个函数来处理结果。

💡 例子:回调函数点咖啡

1
2
3
4
5
6
7
8
9
10
11
function orderCoffee(callback) {
console.log("开始制作咖啡...");
setTimeout(() => {
console.log("咖啡制作完成 ☕");
callback("☕ 咖啡");
}, 2000);
}

orderCoffee(coffee => {
console.log("✅ 你拿到了:", coffee);
});

📌 运行结果

1
2
3
4
开始制作咖啡...
2 秒后)
咖啡制作完成 ☕
✅ 你拿到了: ☕ 咖啡

🔹 回调的缺点

  1. 回调地狱(Callback Hell):
    • 如果有多个步骤(如:点咖啡 → 加糖 → 加奶),代码会变得嵌套很多层,难以维护。
1
2
3
4
5
6
7
orderCoffee(coffee => {
addSugar(coffee, sugaredCoffee => {
addMilk(sugaredCoffee, finalCoffee => {
console.log("✅ 你的最终咖啡:", finalCoffee);
});
});
});
  1. 可读性差,难以调试。

📌 3. 事件循环(Event Loop)—— JavaScript 异步的核心

JavaScript 的 异步任务(如 setTimeout()、Promise、fetch)不是立即执行的,而是放入任务队列,等主线程(同步任务)完成后再执行。这就是事件循环(Event Loop)的工作方式。

💡 事件循环的运作

  1. JavaScript代码会先执行同步任务(如变量声明、函数调用)。
  2. 遇到异步任务(如 setTimeout()),它会交给浏览器(WebAPI)执行,完成后将回调函数放入任务队列。
  3. 当主线程空闲时,事件循环(Event Loop)会把任务队列里的任务放入主线程执行。

💡 例子:事件循环

1
2
3
4
5
6
7
8
console.log("1. 我先来杯咖啡");  // 同步任务

setTimeout(() => {
console.log("3. 咖啡做好了 ☕"); // 异步任务
}, 2000);

console.log("2. 先看看手机等咖啡"); // 同步任务

📌 执行顺序

1
2
3
4
1. 我先来杯咖啡
2. 先看看手机等咖啡
2 秒后)
3. 咖啡做好了 ☕

📌 4. Promise—— 解决回调地狱

Promise 是 异步任务的容器,它有三种状态:

  • pending(等待中)
  • fulfilled(已完成)
  • rejected(已拒绝)

💡 例子:用 Promise 处理点咖啡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function orderCoffee() {
return new Promise((resolve, reject) => {
console.log("开始制作咖啡...");
setTimeout(() => {
resolve("☕ 咖啡"); // 成功
}, 2000);
});
}

orderCoffee()
.then(coffee => {
console.log("✅ 你拿到了:", coffee);
})
.catch(error => {
console.log("❌ 失败:", error);
});

📌 执行顺序

1
2
3
开始制作咖啡...
2 秒后)
✅ 你拿到了: ☕ 咖啡

Promise 的优势:

  • 链式调用,比回调函数更清晰。
  • 错误处理更优雅,可以用 .catch()。

📌 5. async/await—— 最清晰的异步写法

async/await是Promise的语法糖,让代码更像同步代码。

💡 例子:async/await 版的点咖啡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function orderCoffee() {
return new Promise(resolve => {
console.log("开始制作咖啡...");
setTimeout(() => resolve("☕ 咖啡"), 2000);
});
}

async function getCoffee() {
console.log("我点了一杯咖啡...");
let coffee = await orderCoffee(); // 等待咖啡完成
console.log("✅ 我拿到了:", coffee);
}

getCoffee();

📌 执行顺序

1
2
3
4
我点了一杯咖啡...
开始制作咖啡...
(2 秒后)
✅ 我拿到了: ☕ 咖啡

为什么async/await更好?

  • 代码更直观,没有 .then() 链式调用。
  • 结构更清晰,像同步代码一样易读。

📌 6. Promise.all() 和 Promise.race()

🔹 Promise.all() —— 并行执行

如果你同时点 咖啡、蛋糕、面包,可以用 Promise.all() 并行执行:

1
2
3
4
5
6
const coffee = new Promise(resolve => setTimeout(() => resolve("☕ 咖啡"), 2000));
const cake = new Promise(resolve => setTimeout(() => resolve("🍰 蛋糕"), 3000));
const bread = new Promise(resolve => setTimeout(() => resolve("🥪 面包"), 1500));

Promise.all([coffee, cake, bread])
.then(results => console.log("✅ 早餐准备好啦:", results));

📌 输出

1
✅ 早餐准备好啦: ["☕ 咖啡", "🍰 蛋糕", "🥪 面包"]

🔹 Promise.race() —— 谁先完成返回谁

1
2
Promise.race([coffee, cake, bread])
.then(result => console.log("✅ 最快的食物是:", result));

📌 输出

1
✅ 最快的食物是: 🥪 面包