← 返回目录

9. Promise

Promise 想成"奶茶店取号单":你下单后拿到一张纸条(promise 对象),单子上写着"正在做";做好了叫号,你走 .then 取餐;做砸了走 .catch 退款。它把"异步的事"包起来,成功走 then、失败走 catch,而且状态一旦定下来就不能再改

9.1 互动演示(外卖订单模拟)

点按钮看 Promise 的 pending → fulfilled / rejected 流转。

9.2 知识点讲解

① 以前:回调套回调 vs 现在:then 排队

以前异步操作像"传话游戏"——一层套一层,套深了就叫回调地狱.then 改成"排队叫号",一步接一步,平着写。

// 以前:一层套一层,套到头晕(这就是"回调地狱")
setTimeout(function () {          // 第 1 个定时器
  console.log("第一步");          // 先做第一步
  setTimeout(function () {         // 等第一步完了,再套一层定时器
    console.log("第二步");
    setTimeout(function () { console.log("第三步"); }, 300); // 再套一层,深到缩进都歪了
  }, 300);
}, 300);

// 现在:then 排队,一行一步(平着写,不再缩进套娃)
new Promise((resolve) => {          // 造一个新承诺,resolve 是"成功时叫谁"
    setTimeout(() => {                  // 等 300 毫秒
      console.log("第一步");          // 先做第一步
      resolve();                       // 做完了,喊 resolve 通知下一步
    }, 300);
  })
  .then(() => { console.log("第二步"); }) // 第一步做完叫第二步
  .then(() => { console.log("第三步"); }); // 第二步做完叫第三步

② 三个状态(必背)

一张取号单只会经历三种状态,而且只能"从等待中走一次"。

pending—— 等待中( 刚下单, 奶茶还在做)   // 刚拿到取号单,还没结果
fulfilled—— 成功( resolve 触发, 做好了)   // 做成了,resolve 一叫就进这个状态
rejected—— 失败( reject 触发, 做砸了)    // 做砸了,reject 一叫就进这个状态

像外卖订单:刚下单 pending,做好了 fulfilled,做砸了 rejected状态只能从 pending 变一次,定了就改不了——你不能取了餐又退成"还在做"。

③ 成功失败分开写:then / catch

.then 收成功的结果,.catch 收失败的原因,各管各的。

let p = new Promise((resolve, reject) => {   // 造承诺:resolve 管成功,reject 管失败
  setTimeout(() => {                  // 等 300 毫秒后判断成败
    let ok = false;                // 假设这次是失败
    if (ok) resolve("数据");      // 成功:调 resolve 并把结果传出去
    else reject("出错了");         // 失败:调 reject 并把原因传出去
  }, 300);
});
p.then(data => console.log("成功:", data))   // 成功走这里,拿到 data
  .catch(err => console.log("失败:", err)); // 失败走这里,拿到 err

如果你写了 .then 却没写 .catch,又碰上 reject,控制台会冒出一条红色的"Uncaught (in promise)"警告。这就是异步失败没人接的信号——任何可能失败的异步操作,后面都该跟一个 .catch

④ 真实用法:把异步操作包起来

实战里最常见的套路是:把"等一件事"包成 Promise,外面用 then 接。

function delay(ms) {              // 把"等 ms 毫秒"包成一个承诺
  return new Promise(resolve => { // 返回一个新承诺
    setTimeout(resolve, ms);        // ms 毫秒后自动成功(resolve 直接当定时器回调)
  });
}
delay(1000).then(() => console.log("1 秒后执行")); // 等 1 秒,再打印这句

把"等 1 秒"包成 Promise,外面用 .then 接。真实项目就是用这个思路包 Ajax、读文件、加载图片——比如 fetch 本身返回的就是 Promise。

⑤ 状态一旦固定就不能改

let p = new Promise((resolve, reject) => {
  resolve("第一次");   // 先成功,状态变成 fulfilled
  reject("第二次");   // 没用,状态已经是成功了,后面这句直接被忽略
});
p.then(v => console.log(v)); // 第一次

如果你在同一个 Promise 里先调了 resolve 又调 reject,后面那个会被直接忽略。因为取号单一旦"叫过号"就作废,不能反悔——这也是 Promise 比回调靠谱的地方。

9.3 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:封装 fetch/ajax 请求、读取文件/图片、setTimeout 包装成可 await 的延时、多步异步任务排队(登录→拉用户→拉订单)。

② 常见的问题:漏写 .catch 导致静默报错;.then 里忘记 return,下一个 then 拿到 undefined;把同步代码误包进 Promise 以为在异步。

③ 解决思路:每个 Promise 链末尾都加 .catch.then 之间要传值就 return 数据;多个无依赖请求用 Promise.all([p1, p2]) 并发,别串行 await 浪费时间。

一句话:Promise = 把"成功怎么办"和"失败怎么办"分开写的异步工具,状态定一次就改不了。