JavaScript Promise 使用指南:从回调地狱到 async/await

作者:拾光

时间:2026-10-25

浏览:630

本文详解 JavaScript 中 Promise 的核心用法,涵盖状态管理、链式调用及错误处理。通过对比回调地狱,展示 Promise.all、race 等静态方法,并结合 async/await 语法实现简洁的异步代码,帮助开发者高效处理复杂异步逻辑。

文章目录

前言

回顾初学 JavaScript 时,我对 Promise 的诸多概念感到困惑。如今工作已逾半年,特此整理笔记及实际工作中的应用场景,撰写此文以作记录(注:本文仅聚焦于使用方式,暂不深入探讨底层原理,若有疏漏敬请指正)。

Promise 简介

Promise(亦称期约)是 ES6 规范引入的一种异步编程解决方案,旨在有效解决异步函数嵌套过深导致的“回调地狱”问题。

什么是回调地狱?

假设存在一个业务需求:获取“用户数据3”,该数据依赖于“用户数据2”,而“用户数据2”又依赖于“用户数据1”。因此,正确的数据获取顺序为:用户数据1 -> 用户数据2 -> 用户数据3。以下模拟使用回调函数进行编写的代码示例:

Node 接口代码:

const router = require('express').Router();

const data1 = { data: "用户数据1" };
router.get('/testData1', (req, res) => {
    res.json(data1);
})

const data2 = { data: "用户数据1,用户数据2" };
router.get('/testData2', (req, res) => {
    if (req.query.data === data1.data) {
        res.json(data2);
    } else {
        res.status(401).json("参数错误");
    }
})

router.get('/testData3', (req, res) => {
    if (req.query.data === data2.data) {
        res.json({ data: "用户数据1,用户数据2,用户数据3" });
    } else {
        res.status(401).json("参数错误");
    }
})

module.exports = router;

前端请求代码:

// 简单封装的 XMLHttpRequest 请求函数
const baseUrl = "http://localhost:8888/test";
const request = (url, cb) => {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', baseUrl + url);
  xhr.send();
  xhr.onreadystatechange = () => {
    const { readyState, status, response } = xhr;
    if (readyState === 4) {
      if (status >= 200 && status <= 299) {
        cb(JSON.parse(response));
      } else {
        throw new Error(response);
      }
    }
  }
}

// 因为下一个请求需要上一个请求的数据所以需要一个回调函数嵌套一个回调函数
request("/testData1", res1 => {
  console.log(res1); // => {data: '用户数据1'}
  request(`/testData2?data=${res1.data}`, res2 => {
    console.log(res2); // => {data: '用户数据1,用户数据2'}
    request(`/testData3?data=${res2.data}`, res3 => {
      console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
      // ....
    })
  })
})

这种层层嵌套的代码阅读体验极差,若需求更为复杂,其结构之繁琐可见一斑(此处引用一张网络图片示意,来源已不可考)。

前言

此类一个回调嵌套另一个回调的代码,可读性与可维护性均极差,业界称之为“回调地狱”。Promise 的引入正是为了解决这一痛点。

Promise 的特点

Promise 对象拥有一个内部状态,该状态不受外部因素干扰,主要分为以下三种状态:

  • Pending(进行中/待定):初始状态。
  • Fulfilled(成功/兑现):操作成功完成。
  • Rejected(失败/拒绝):操作失败。

下文统一使用“成功”和“失败”来指代这两种状态。

一旦 Promise 对象的状态发生转换(变为成功或失败),便不可逆转,状态转换是单向的:

Pending(进行中) -> Fulfilled(成功)。

Pending(进行中) -> Rejected(失败)。

创建 Promise 实例

创建 Promise 实例需使用 new Promise 构造函数,该构造函数接收一个处理器函数作为参数。该处理器函数接收两个参数:resolve 和 reject(名称可自定义,建议语义化),它们均为内置函数,无需手动实现。示例如下:

const p = new Promise((resolve, reject) => {

})
console.log(p);

前言

由输出可见,默认状态下 Promise 的状态为 Pending(进行中)。

在处理器函数内部调用 resolve 或 reject 函数(并可传递参数),即可改变 Promise 对象的状态:

const p = new Promise((resolve, reject) => {
  resolve();
})
console.log(p);

前言

调用 resolve 函数会将 Promise 状态修改为 Fulfilled(成功);调用 reject 函数则将状态修改为 Rejected(失败)。

then 方法

Promise 实例的 then 方法接收两个函数作为参数,分别用于指定状态变为成功或失败时调用的回调函数(注意:Promise 的 then 方法是异步执行的微任务):

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功 
  resolve();
})
    
console.log("同步代码");
p.then(
  () => {
    console.log("成功的回调");
  },
  () => {
    console.log("失败的回调");
  }
)

运行结果如下:

前言

反之,若调用 reject 函数,则会触发 then 方法的第二个回调函数。若同时调用 resolve 和 reject,仅最先调用的那一个生效(因状态转换具有单向性)。

resolve 和 reject 的参数传递

通过上述测试可知,resolve 和 reject 函数不仅能修改状态,还能触发 Promise 的 then 回调。既然它们是函数,自然支持参数传递,该参数将被传递给对应的 then 方法回调函数接收:

const p = new Promise((resolve, reject) => {
  // 将p的状态修改为成功, 并且传递一个参数
  resolve("ok");
})

console.log("同步代码");
p.then(
  // 这里接收 resolve 函数传递的参数
  res => {
    console.log("成功的回调", res);
  },
  // 这里接收 reject 函数传递的参数
  err => {
    console.log("失败的回调");
  }
)

运行结果如下:

前言

then() 链式调用

理解 then 方法后,我们可以对前述需求进行改写:

// 请求函数使用 Promise 封装
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
  // 返回一个Promise实例
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', baseUrl + url);
    xhr.send();
    xhr.onreadystatechange = () => {
      const { readyState, status, response } = xhr;
      if (readyState === 4) {
        if (status >= 200 && status <= 299) {
          const res = JSON.parse(response);
          // 修改状态为成功并且把响应传递过去
          resolve(res);
        } else {
          // 修改状态为失败也把响应传递过去
          reject(response);
        }
      }
    }
  })
}

// 使用then方法
request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    request(`/testData2?data=${res1.data}`).then(
      res2 => {
        console.log(res2); // {data: '用户数据1,用户数据2'}
        request(`/testData3?data=${res2.data}`).then(
          res3 => {
            console.log("需求需要的数据", res3); // => 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
          }
        )
      }
    )
  }
)

乍看之下,这种写法似乎并未比回调函数更简洁,甚至显得冗余。换个思路:若在 then 方法中返回一个新的 Promise 实例,是否就可以继续链式调用 then 了呢?代码如下:

const p1 = new Promise((resolve, reject) => {
  resolve("p1数据");
})

// 这里的p2就是p1.then方法成功回调里面返回的p2
const p2 = p1.then(
    // 这里的res1参数就是p1的处理器函数中调用 resolve("p1数据") 传递的参数
    res1 => {
      console.log(res1); // p1数据

      // 这里新建一个新的Promise实例, 记作p2
      const p2 = new Promise((resolve, reject) => {
        // 0.5s后修改状态
        setTimeout(() => {
          resolve(res1 + ",p2数据");
        }, 500);

      })
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // p1数据,p2数据
    // 这里和上面的同理
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res2 + ",p3数据");
      }, 500);

    })
    return p3;
  }
)

p3.then(
  res3 => {
    console.log(res3); // p1数据,p2数据,p3数据
  }
)

验证可行后,进一步优化需求代码:

// 这里的p1就是 request("/testData1") 返回的Promise实例
const p1 = request("/testData1");

// 这里的p2就是p1.then方法成功回调返回的p2
const p2 = p1.then(
    res1 => {
      console.log(res1); // {data: '用户数据1'}

      // 再调用request方法将其返回的Promise实例记作p2
      const p2 = request(`/testData2?data=${res1.data}`);
      // 将p2返回
      return p2;
    }
  )

const p3 = p2.then(
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 这里和上面的同理
    const p3 = request(`/testData3?data=${res2.data}`);
    return p3;
  }
)

p3.then(
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

虽然实现了需求,但代码仍显冗余,可进一步精简如下:

request("/testData1").then(
  res1 => {
    console.log(res1); // {data: '用户数据1'}
    // 这里直接返回 request方法的返回值Promise实例
    return request(`/testData2?data=${res1.data}`);
  }
).then( 
  // 这个成功回调时上一个then方法返回的Promise实例的成功回调
  res2 => {
    console.log(res2); // {data: '用户数据1,用户数据2'}
    // 同理
    return request(`/testData3?data=${res2.data}`);
  }
).then(
  // 同理
  res3 => {
    console.log("需求需要的数据", res3); // 需求需要的数据 {data: '用户数据1,用户数据2,用户数据3'}
  }
)

上述代码结构形似链条,因此被称为“链式调用”。

then() 的返回值

经前述测试,then 方法除返回 Promise 对象外,还可返回任意值。若返回非 Promise 值,该值会被转换为 Promise 对象,其内部状态视返回值而定:

const p1 = new Promise(resolve => resolve());
p1.then(() => {
  // 这里相当于是 return undefined
}).then(
  res1 => {
    console.log(res1); // undefined
    return "hello";
  }
).then(
  res2 => {
    console.log(res2); // hello
    return { name: "张三" };
  }
).then(
  res3 => {
    console.log(res3); // { name: "张三" }

    // 返回错误对象该Promise对象的状态也是成功
    return new Error("error object");
  }
).then(
  res4 => {
    console.log(res4 instanceof Error); // true
    console.log(res4.message); // error object

    // 抛出一个错误则该Promise对象的状态是失败
    throw "thorw error";
  }
).then(
  () => { },
  err => {
    console.log(err); // thorw error
  }
)

catch 方法

虽然 then 的链式调用解决了回调嵌套问题,但尚未处理请求失败的情况。虽然 then 方法的第二个参数可指定失败回调,但通常推荐使用 catch 方法来统一处理错误,使用格式如下:

p1.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch可以捕获这一条调用链上的错误
    // 处理错误
})

finally 方法

与 catch 对应的是 finally 方法,其作用类似于 try...catch...finally 中的 finally 块,使用格式如下:

p.then(
  // ...
).then(
  // ...

).catch(err => { // 这个catch方法可以捕获这一条调用链上的错误
  // 处理错误
    
}).finally(() => { // 无论成功还是失败这个finally中指定的回调都会执行
  // 清除loading, 重置状态...
})

Promise 的静态方法

Promise.resolve()

该方法立即返回一个状态为成功(Fulfilled)的 Promise 对象。可传递参数,该参数将被返回 Promise 实例的 then 方法回调(异步微任务)接收:

const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res)); // Promise.resolve

也可利用 Promise.resolve() 来创建一个微任务:

console.log("同步代码");

setTimeout(() => console.log("setTimeout"), 0);

const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res));

运行结果如下:

前言

Promise.reject()

与 Promise.resolve() 类似,但返回的状态为失败(Rejected)的 Promise 对象(同样作为微任务执行)。

console.log("同步代码");

setTimeout(() => console.log("setTimeout"), 0);

const p = Promise.reject("Promise.reject");
p.then().catch(err => console.log("Promise.reject"));

Promise.all()

Promise.all 接收一个可迭代对象(如 Array、Map、Set 等 ES6 的 iterable 类型),其中包含多个 Promise 实例。Promise.all 会等待所有 Promise 对象完成(或第一个失败),并根据给定参数返回相应的结果。

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

// 依次打印数据
p1.then(res => console.log(res)); // 0.5s 后打印 p1 data
p2.then(res => console.log(res)); // 1.0s 后打印 p2 data
p3.then(res => console.log(res)); // 1.5s 后打印 p3 data


const proArray = [p1, p2, p3];
Promise.all(proArray).then(resList => {
  console.log(resList); // 1.5s后打印数据 ['p1 data', 'p2 data', 'p3 data']
}).catch(err => {
  // 如果在`Promise.all`方法中出现了失败的状态, 那么这个参数会是这个失败状态返回的参数(如果有的话)
  console.error("error: ", err); 
})

利用 Promise.all() 方法的特性,可同时发送多个请求,示例如下:

Node 接口代码:

const getRandom = () => Math.random() * 9 + 1;
router.get('/testData4', (req, res) => {
    let n = req.query.n;
    const random = getRandom();

    // 定时器模拟接口响应时间差
    setTimeout(() => {
        res.json(`第${++n}个请求${random}`);
    }, random * 50);
});

前端发送多个请求:

const proArray = [];
for (let i = 0; i < 10; i++) {
  // 将10个请求方法返回的Promsie对象添加到proArray数组中
  proArray.push(request(`/testData4?n=${i}`));
}
Promise.all(proArray).then(resList => {
  for (const res of resList) {
    // 每个请求的返回结果
    console.log(res);
  }
})

Promise.allSettled()

Promise.allSettled() 方法与 Promise.all() 类似,区别在于无论期约对象成功还是失败,都会触发 then 方法的成功回调。每个期约返回一个对象,status 属性表示状态,value 表示成功回调的值;若状态为失败,则失败回调的值存储在 reason 属性中:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
  // p2的Promise实例的状态修改为失败
  setTimeout(() => reject("p2 err"), 1000);
})

const proArray = [p1, p2];
Promise.allSettled(proArray).then(resList => {
  console.log(resList); // (2) [{…}, {…}]

  for (const res of resList) {
    const { status, value, reason } = res;
    if (reason) {
      console.log(`失败: ${status}, 原因是: ${reason}`); // 失败: rejected, 原因是: p2 err
    } else { 
      console.log(`成功: ${status}, 数据是: ${value}`); // 成功: fulfilled, 数据是: p1 data
    }
  }
}).catch(err => {
  // 这里不会捕获p2的失败的回调 
  console.error("error: ", err);
})

Promise.race()

Promise.race() 与 Promise.all() 类似,均接收可迭代参数,但不同之处在于:

Promise.race() 的状态变化不受所有参数状态影响,一旦迭代器中某个 Promise 解决或拒绝,返回的 Promise 就会随之解决或拒绝。

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => reject("p2 err"), 1000);
})

const proArray = [p1, p2];
Promise.race(proArray).then(res => {
    console.log(res); // p1 data
}).catch(err => {
    console.error(err);
})

async 和 await

async 函数

使用 async 关键字声明的函数(亦称异步函数)与普通函数在用法上无明显区别:

// 函数声明
async function asyncFn1() {
  console.log("asyncFn1");
}

// 函数表达式
const asyncFn2 = async () => {
  console.log("asyncFn2");
}

asyncFn1();
asyncFn2();

// 立即调用
(async () => {
  console.log("asyncFn3");
})();

await

await 操作符用于等待一个 Promise 对象,它只能在 async function 中使用。结合 async 与 await 可将异步代码“变”得如同同步代码一般:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("data");
  }, 1000);
});

// async 函数
async function asyncFn() {
  console.log("asyncFn函数开始执行");

  // await 会等待右边的Promise对象的状态变成功后返回其值
  const res = await p;
  console.log(res); // data

  console.log("asyncFn函数执行完了");
}

// 调用
asyncFn();

上述代码先输出“asyncFn 函数开始执行”,随后等待约 1 秒输出“data”,最后输出“asyncFn 函数执行完了”。

注意:异步函数不会阻塞主线程的执行,它是异步的:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("data");
  }, 1000);
});

// async 函数
async function asyncFn() {
  console.log("asyncFn函数开始执行");
  const res = await p;
  console.log(res); 
  console.log("asyncFn函数执行完了");
}

console.log("hello");
asyncFn(); 
console.log("javascript");

等待约 1 秒后,上述代码执行结果如下:

前言

根据执行结果,我们发现异步函数内的 await 关键字会等待其右侧 Promise 对象的返回值,待等待结束后,后续代码才会执行。利用此特性,可在 JavaScript 中实现类似 Java 的 Thread.sleep() 方法,如下:

const sleep = async time => new Promise(resolve => setTimeout(resolve, time));

(async () => {
  console.log("1");
  await sleep(1000);
  console.log("2");
  await sleep(500);
  console.log("2.5");
})();

理解 async 和 await 的用法后,我们可以使用异步函数重新完成最初的需求:

// 请求函数使用 Promise 封装
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
  // 返回一个Promise实例
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', baseUrl + url);
    xhr.send();
    xhr.onreadystatechange = () => {
      const { readyState, status, response } = xhr;
      if (readyState === 4) {
        if (status >= 200 && status <= 299) {
          const res = JSON.parse(response);
          // 修改状态为成功并且把响应传递过去
          resolve(res);
        } else {
          // 修改状态为失败也把响应传递过去
          reject(response);
        }
      }
    }
  })
}

async function asyncFn() {
  const res1 = await request("/testData1");
  console.log(res1); // {data: '用户数据1'}

  const res2 = await request(`/testData2?data=${res1.data}`);
  console.log(res2); // {data: '用户数据1,用户数据2'}

  const res3 = await request(`/testData3?data=${res2.data}`);
  console.log(res3); // {data: '用户数据1,用户数据2,用户数据3'}
}

asyncFn();

可见,使用 async 和 await 发送网络请求的代码既简洁又直观。

异步函数的错误处理

异步函数的异常处理可使用 try...catch 和 catch 进行处理:

try...catch

async function asyncFn() {
  let res1, res2, res3;
  try {
    res1 = await request("/testData1");

    try {
      if (res1?.data) {
        res2 = await request(`/testData2?data=${res1.data}`);
      }

      try {
        if (res2?.data) {
          res3 = await request(`/testData3?data=${res2.data}`);
        }

      } catch (error) {
        // 处理res3 error
      }

    } catch (error) {
      // 处理res2 error
    }

  } catch (error) {
    // 处理res3 error
  }
}

catch

async function asyncFn() {
  const res1 = await request("/testData1")
    .catch(err => {
      // 处理res1 error
    });

  if (res1?.data) {
    const res2 = await request(`/testData2?data=${res1.data}`)
      .catch(err => {
        // 处理res2 error
      });

    if (res2?.data) {
      const res3 = await request(`/testData3?data=${res2.data}`)
        .catch(err => {
          // 处理res3 error
        });
    }
  }
}

异步函数同样适用于 Promise 的一些静态方法。

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

const proArray = [p1, p2, p3];
async function asyncFn() {
  const list = await Promise.all(proArray);
  console.log(list); // ['p1 data', 'p2 data', 'p3 data']
}
asyncFn();

for await...of

若数组中存储多个 Promise 对象,我们可以通过 Promise.all 获取,或通过循环等待其返回值。使用循环示例如下:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("p3 data"), 1500);
})

const proArray = [p1, p2, p3];

async function asyncFn() {
  for (const pro of proArray) {
    // 这里不要忘记 await
    const res = await pro;
    console.log(res);
  }
}
asyncFn();

ES9 引入的新语法 for await..of 可自动等待每次循环项的返回值。

async function asyncFn() {
  for await (const item of proArray) {
    console.log(item);
  }
}
asyncFn();

参考:

  • MDN Promise
  • MDN async
  • MDN for await...of
  • JavaScript 权威指南
  • 阮一峰 ES6 入门之 promise

总结

至此,关于 JavaScript 中 Promise 使用的介绍到此结束。如需了解更多 JS Promise 相关内容,请搜索本站往期文章或继续浏览下文。感谢大家的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章