JavaScript 异步编程完全指南:从回调地狱到 async/await,一次通关

JavaScript 异步编程完全指南:从回调地狱到 async/await,一次通关

你也许每天都在用 Promiseasync/await,但闭眼能说清事件循环、微任务、宏任务的执行顺序吗?

为什么 setTimeout 明明是 0 毫秒,却要等到最后才执行?
更重要的是:日常开发中到底该用 Promise 还是 async/await?

本文从零开始,手写代码、画图讲解、逐层进阶,带你彻底掌握 JavaScript 异步编程,并给出实战最佳实践。


目录

  1. 为什么需要异步?
  2. 回调函数:最朴素的异步方案
  3. Promise:拯救回调地狱
  4. [Promise 静态方法与链式技巧](#Promise 静态方法与链式技巧 "#4-promise-%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E4%B8%8E%E9%93%BE%E5%BC%8F%E6%8A%80%E5%B7%A7")
  5. async/await:用同步的写法写异步
  6. [事件循环:微任务 vs 宏任务](#事件循环:微任务 vs 宏任务 "#6-%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E5%BE%AE%E4%BB%BB%E5%8A%A1-vs-%E5%AE%8F%E4%BB%BB%E5%8A%A1")
  7. [实战建议:开发中该用 Promise 还是 async/await?](#实战建议:开发中该用 Promise 还是 async/await? "#7-%E5%AE%9E%E6%88%98%E5%BB%BA%E8%AE%AE%E5%BC%80%E5%8F%91%E4%B8%AD%E8%AF%A5%E7%94%A8-promise-%E8%BF%98%E6%98%AF-asyncawait")
  8. [手写一个简易 Promise(进阶)](#手写一个简易 Promise(进阶) "#8-%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93-promise%E8%BF%9B%E9%98%B6")
  9. 总结与面试题挑战

1. 为什么需要异步?

JavaScript 是单线程 语言,同一时间只能做一件事。如果某个任务耗时很长(比如网络请求、文件读取、定时器),整个程序就会卡住,用户界面无法点击。为了避免这种情况,JS 把耗时操作设计成异步:先不等待结果,继续执行后面的代码,等结果回来了再处理。

生活类比

  • 同步(阻塞):你去食堂打饭,排队时只能干等,直到轮到你。
  • 异步(非阻塞):你点完餐拿到号,先去占座刷手机,听到叫号再去取餐。

2. 回调函数:最朴素的异步方案

回调函数就是把一个函数作为参数传给另一个函数,当异步操作完成时调用这个函数。

2.1 定时器回调

js 复制代码
console.log('开始');
setTimeout(() => {
  console.log('2秒后执行');
}, 2000);
console.log('结束');
// 输出:开始 → 结束 → (2秒后)2秒后执行

2.2 模拟网络请求

js 复制代码
function fetchUser(callback) {
  setTimeout(() => {
    const user = { id: 1, name: '张三' };
    callback(user);
  }, 1000);
}

fetchUser((user) => {
  console.log(user); // { id: 1, name: '张三' }
});

2.3 回调地狱(Callback Hell)

当需要串行执行多个异步操作时,回调层层嵌套,可读性极差:

js 复制代码
getUser(1, (user) => {
  getOrders(user.id, (orders) => {
    getProducts(orders[0].id, (products) => {
      console.log(products);
    });
  });
});

这就是"回调地狱",横向发展,错误处理困难,难以维护。


3. Promise:拯救回调地狱

Promise 是一个代表了未来某个结果的对象,它有三种状态pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态只能改变一次,且一旦改变就不可逆。

3.1 创建 Promise

js 复制代码
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('成功的数据');
    } else {
      reject('失败原因');
    }
  }, 1000);
});

promise
  .then((data) => console.log(data))
  .catch((err) => console.error(err));
  • resolve:将状态从 pending → fulfilled,并传递结果。
  • reject:将状态从 pending → rejected,并传递错误原因。

3.2 把回调函数改写成 Promise

js 复制代码
function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) resolve({ id: 1, name: '张三' });
      else reject(new Error('用户不存在'));
    }, 500);
  });
}

function getOrders(userId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 101, name: '订单A' }]), 500);
  });
}

// 链式调用
getUser(1)
  .then(user => {
    console.log(user);
    return getOrders(user.id);
  })
  .then(orders => {
    console.log(orders);
  })
  .catch(err => console.error(err));

3.3 链式调用的关键

then 方法总是返回一个新的 Promise,因此可以无限链下去。

  • 如果 then 的回调返回一个普通值,新 Promise 会以该值 resolve。
  • 如果返回一个 Promise,新 Promise 会等待这个 Promise 完成。
  • 如果抛出异常,新 Promise 会 reject。
js 复制代码
Promise.resolve(1)
  .then(x => x + 1)           // 返回 2
  .then(x => { throw new Error('出错了'); })
  .catch(err => console.log(err));

4. Promise 静态方法与链式技巧

4.1 Promise.resolve()Promise.reject()

快速创建已成功/失败的 Promise。

js 复制代码
Promise.resolve('直接成功').then(console.log);
Promise.reject('直接失败').catch(console.log);

4.2 Promise.all():等待所有完成

并行执行多个 Promise,全部成功才成功,任一失败则失败。

js 复制代码
const p1 = fetch('/api/user');
const p2 = fetch('/api/orders');
Promise.all([p1, p2])
  .then(([user, orders]) => console.log(user, orders))
  .catch(err => console.error(err));

4.3 Promise.race():取最快的一个

js 复制代码
const timeout = new Promise((_, reject) => setTimeout(() => reject('超时'), 3000));
const request = fetch('/api/data');
Promise.race([request, timeout])
  .then(data => console.log(data))
  .catch(err => console.log(err));

4.4 Promise.allSettled():等待所有结束(无论成功失败)

js 复制代码
const promises = [
  Promise.resolve('成功'),
  Promise.reject('失败'),
  Promise.resolve('也成功')
];
Promise.allSettled(promises).then(results => {
  results.forEach(result => console.log(result.status));
});

4.5 Promise.any():任意一个成功即成功(全部失败才失败)

js 复制代码
Promise.any([
  Promise.reject('错误1'),
  Promise.resolve('成功'),
  Promise.reject('错误2')
]).then(console.log); // 输出 '成功'

5. async/await:用同步的写法写异步

async 函数返回一个 Promise,await 等待 Promise 完成。它让异步代码变得像同步一样直观。

5.1 基本用法

js 复制代码
async function fetchData() {
  const user = await getUser(1);
  const orders = await getOrders(user.id);
  console.log(orders);
}
fetchData();

5.2 错误处理:try/catch

js 复制代码
async function fetchData() {
  try {
    const user = await getUser(1);
    const orders = await getOrders(user.id);
    console.log(orders);
  } catch (err) {
    console.error('出错了', err);
  }
}

5.3 并行执行:不要滥用 await

下面这个例子是串行,总耗时 = 两个请求时间之和:

js 复制代码
const user = await getUser(1);
const orders = await getOrders(user.id);  // 必须等 user 完成

如果要并行,使用 Promise.all

js 复制代码
const [user, posts] = await Promise.all([getUser(1), getPosts(1)]);

5.4 顶层 await(ES2022)

在模块中可以直接使用 await,不需要包裹 async 函数。

html 复制代码
<script type="module">
  const data = await fetch('/api/data').then(res => res.json());
  console.log(data);
</script>

6. 事件循环:微任务 vs 宏任务

光会用 Promise 还不够,要理解执行顺序,才能避开面试陷阱。

6.1 宏任务(MacroTask)和微任务(MicroTask)

  • 宏任务 :整体代码块、setTimeoutsetInterval、I/O、UI 渲染、setImmediate(Node.js)
  • 微任务Promise.then/catch/finallyMutationObserverqueueMicrotaskprocess.nextTick(Node.js)

事件循环流程

  1. 执行一个宏任务(第一次是全局脚本)。
  2. 执行当前宏任务产生的所有微任务(清空微任务队列)。
  3. 必要时渲染页面。
  4. 取出下一个宏任务执行,重复。

6.2 经典示例

js 复制代码
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

输出1 4 3 2

解释:同步代码(1,4)先执行 → 清空微任务(3)→ 宏任务(2)

6.3 async/await 与事件循环

js 复制代码
async function foo() {
  console.log('2');
  await bar();
  console.log('4');
}
async function bar() {
  console.log('3');
}
console.log('1');
foo();
console.log('5');

输出:1 2 3 5 4
关键await bar() 后面的代码(console.log('4'))相当于被 Promise.resolve(bar()).then(() => console.log('4')) 包裹,所以进入微任务队列。


7. 实战建议:开发中该用 Promise 还是 async/await?

很多新手面对 Promise 的 thencatchfinallyallrace... 觉得方法太多记不住。其实你完全不用担心,日常开发中 90% 的场景用 async/await 就够了

7.1 对比:Promise vs async/await

对比 Promise async/await
写法 链式调用 .then(),容易嵌套 像同步代码一样从上往下写
错误处理 .catch() 容易漏或位置不对 try/catch,和 Python/Java 一样
调试 断点不好打,堆栈信息有时乱 断点打在 await 行,清晰直观
组合异步 Promise.all 仍然需要,但可以 await 复杂流程仍需 Promise 静态方法

7.2 你只需要记住这几个 Promise 方法就够了

方法 场景 记忆点
async/await 代替 then/catch 日常写异步就用它
Promise.all 并发请求,全部成功才算成功 "等所有人都到齐"
Promise.race 超时控制(谁快用谁) "赛跑,第一名决定结果"
Promise.resolve / reject 快速返回一个已完成/失败的 Promise 偶尔用于测试或封装

其他方法(allSettledanyfinally)遇到具体需求时再查,不用硬记。

7.3 正确的学习与使用路径

  1. 先掌握 async/await

    js 复制代码
    async function getData() {
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        console.log(data);
      } catch (err) {
        console.error(err);
      }
    }

    这能覆盖 80% 的异步场景。

  2. 再学 Promise.all

    js 复制代码
    const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);

    这是并发请求的标准写法。

  3. 最后了解 Promise.race(超时控制)

    js 复制代码
    const timeout = new Promise((_, reject) => setTimeout(() => reject('超时'), 3000));
    const data = await Promise.race([fetchData(), timeout]);

绝大多数前端开发者不需要自己实现 Promise 或手写 then 链。

业务代码中用 async/await + try/catch + Promise.all 就够了。

面试考手写 Promise 是为了考察底层理解,但不代表工作中要那样写。

所以,现在你可以放心地用 async/await 写所有异步逻辑,只在需要并发或竞赛时借助 Promise.all / Promise.race。这样你只需要记两个关键字(asyncawait)和一个方法(Promise.all),负担直接减半。


8. 手写一个简易 Promise(进阶)

理解 Promise 原理,面试加分项。下面实现一个简化版(不处理链式异步返回 Promise 等高级情况,但足够说明核心)。

js 复制代码
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        }, 0);
      } else if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        }, 0);
      } else {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolve(x);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolve(x);
            } catch (err) {
              reject(err);
            }
          }, 0);
        });
      }
    });
    return promise2;
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

使用示例:

js 复制代码
const p = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('hello'), 1000);
});
p.then(console.log);

9. 总结与面试题挑战

9.1 核心知识点回顾

  • 异步:不阻塞主线程,通过回调、Promise、async/await 处理。
  • Promise:三种状态、链式调用、静态方法(all, race, allSettled, any)。
  • async/await:同步写法,配合 try/catch 处理错误,是日常开发首选。
  • 事件循环 :微任务先于宏任务,Promise.then 是微任务,setTimeout 是宏任务。
  • 实战建议 :用 async/await + Promise.all 覆盖绝大多数场景,不必死记所有 Promise API。

9.2 挑战题(评论区留下你的答案)

js 复制代码
async function test() {
  console.log(1);
  await Promise.resolve();
  console.log(2);
}
console.log(3);
test();
console.log(4);
new Promise((resolve) => {
  console.log(5);
  resolve();
}).then(() => console.log(6));

输出顺序是什么? (答案:3 1 5 4 2 6


如果你能解释清楚上述输出,并能在日常开发中熟练运用 async/await + Promise.all,异步这一关你就彻底过了。下一篇我们将进入事件循环深度解析原型与 class,你想先看哪个?评论区告诉我~


参考资源

相关推荐
kyriewen2 小时前
面试官让我手写Promise,我打开Cursor三秒生成,他愣了两秒说“你过了”
前端·javascript·面试
Bacon2 小时前
RAG 从入门到入土:Agent 时代,你的检索增强生成到底行不行?
前端·人工智能
软件开发技术深度爱好者3 小时前
HTML实现DOCX文档版题库图文考试系统(修订)
前端·javascript·html
宁雨桥3 小时前
从跨项目预览到分层架构:一次 `postMessage` 封装的深度思考
前端·架构·postmessage
蝎子莱莱爱打怪3 小时前
我花两年业余时间做了个IM系统,然后呢😂??
后端·flutter·面试
问征夫以前路3 小时前
Promise知识点回顾
前端·javascript
行走的陀螺仪3 小时前
JavaScript 算法详解:10大经典算法,通俗易懂,从入门到精通
开发语言·javascript·算法
努力成为AK大王3 小时前
Java并发线程核心知识(一)
java·开发语言·面试
拓荒牛儿3 小时前
前端内存可观测实践
前端