Promise详解

Promise是一种异步编程方案。Promise 对象表示异步操作最终的完成(或失败)以及其结果值。

一个 Promise 是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise以便在将来的某个时间点提供该值。

状态

一个Promise有以下三种状态:

  • pending:待定状态,既不是成功,也不是失败状态。
  • settled敲定状态
    • fulfilled:已兑现状态,操作成功完成。
    • rejected:已拒绝状态,操作失败。

Promise对象内部运行的变化:

  1. new Promise()后,表示Promise进入pending初始化状态,准备就绪,等待运行。
  2. 一旦Promise实例运行成功或失败,实例状态就会变更为敲定状态(fulfilled或者rejected),此时状态无法再变更

使用

在使用 Promise 时,通常会调用其 then() 方法来处理异步操作成功的结果,或者调用 catch() 方法来处理出错信息。同时,Promise 还提供了一些静态方法,如 Promise.resolve()Promise.reject() 等用于快速创建一个 Promise 实例。

Thenable对象

Promise 成为 JavaScript 语言的一部分之前,JavaScript 生态系统已经有了多种 Promise 实现。尽管它们在内部的表示方式不同,但至少所有类 Promise 的对象都实现了 Thenable

简单来说,thenable 对象就是实现了 then() 方法的普通对象,该方法被调用时需要传入两个回调函数,一个用于 Promise兑现时调用,一个用于 Promise拒绝时调用。Promise 也是 thenable 对象。

const aThenable = {
  then(onFulfilled, onRejected) {
    ...
  },
};

构造Promise

原型

const promise1 = new Promise(executor);
const promise1 = new Promise((resolve, reject)=>{...});

参数

  • executor,在构造函数中执行的 function。它接收两个函数作为参数resolve (兑现回调)和 reject拒绝回调)。executor 中抛出的任何错误都会导致 Promise拒绝,并且返回值将被忽略。

注:

executor 的对 Promise 的状态的影响:

  • executor 函数中的 return 语句仅影响控制流程,调整函数某个部分是否执行,但不会影响 Promise兑现值。如果 executor 函数退出,且未来不可能调用 resolvereject(例如,没有安排异步任务),那么 Promise 将永远保持待定状态。
  • 如果在 executor 函数中抛出错误,则 Promise 将被拒绝,除非 resolveFuncrejectFunc 已经被调用。
  • executor 的**返回值将作为resolve的参数抛出的错误将作为reject的参数**。

创建后的Promis实例中有then()catch()等方法:

  • then():方法需传递一个参数方法,此参数方法将作为executorresolve,在Promise兑现后执行。
  • catch():方法需传递一个参数方法,此参数方法将作为executorreject,在Promise拒绝后执行。

示例1

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

promise1.then((value) => {  //then方法中的传入的参数方法将作为resolve
  console.log(value);
});

// 0.3秒后输出: "foo"

示例2

const readFilePromise = (path) =>
  new Promise((resolve, reject) => {
    readFile(path, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });

readFilePromise("./data.txt")
  .then((result) => console.log(result))
  .catch((error) => console.error("读取文件失败"));

resolvereject 回调仅在 executor 函数的作用域内可用,在构造 promise 之后无法访问它们。

返回值

当通过 new 关键字调用 Promise 构造函数时,它会返回一个 Promise 对象。当 resolve 或者 reject 被调用时,该 promise 对象就会固定兑现(fulfilled)或拒绝(reject),并立即将resolvereject函数封装为任务推入微队列

请注意,如果你调用 resolvereject 时传入另一个 promise 对象作为参数,可以说该 promise 对象“已解决”,但仍未“敲定settled)”。有关更多解释,请参阅 Promise 描述

Promise编程与传统异步编程区别

以模拟获取用户信息->获取用户订单->获取用户订单详情的三层异步操作为例。

不使用 Promise:回调地狱的问题

传统异步编程依赖回调函数,每一步异步操作的结果需要在回调中处理,多层嵌套会导致代码像 “金字塔” 一样臃肿(回调地狱)。

// 模拟异步(回调模式):获取用户信息
function getUser(userId, callback) {
  setTimeout(() => {
    console.log("获取用户信息成功");
    callback({ id: userId, name: "张三" }); // 回调返回用户数据
  }, 1000);
}

// 模拟异步(回调模式):根据用户信息获取订单编号
function getOrders(user, callback) {
  setTimeout(() => {
    console.log("获取订单成功");
    callback([{ orderId: 1001, userId: user.id }]); // 回调返回订单数据
  }, 1000);
}

// 模拟异步(回调模式):根据订单获取详情
function getOrderDetail(order, callback) {
  setTimeout(() => {
    console.log("获取订单详情成功");
    callback({ orderId: order.orderId, product: "手机" }); // 回调返回详情
  }, 1000);
}

// 业务逻辑:三层嵌套调用
getUser(1, (user) => {  //1.获取用户数据
  getOrders(user, (orders) => {  //2.根据用户数据获取订单编号
    getOrderDetail(orders[0], (detail) => {  //3.根据订单编号获取订单详情
      console.log("最终结果:", detail); // 输出:{ orderId: 1001, product: "手机" }
    });
  });
});

使用 Promise:链式调用的优势

Promise 通过状态管理链式调用,将嵌套的回调改为线性代码,解决了回调地狱的问题。

// 改造为返回 Promise 的异步函数
function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("获取用户信息成功");
      resolve({ id: userId, name: "张三" }); // 成功时用 resolve 返回结果
    }, 1000);
  });
}

function getOrders(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("获取订单成功");
      resolve([{ orderId: 1001, userId: user.id }]);
    }, 1000);
  });
}

function getOrderDetail(order) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("获取订单详情成功");
      resolve({ orderId: order.orderId, product: "手机" });
    }, 1000);
  });
}

// 业务逻辑:链式调用
getUser(1)
  .then((user) => getOrders(user)) // 第一步成功后,调用第二步
  .then((orders) => getOrderDetail(orders[0])) // 第二步成功后,调用第三步
  .then((detail) => {
    console.log("最终结果:", detail); // 输出:{ orderId: 1001, product: "手机" }
  });

链式调用的核心

thencatchfinally 都会返回一个新的 promise 对象,这是链式调用的基础。promise 的状态由回调函数的返回值决定:

  • 若回调返回非 promise 值(如数字、字符串),新 Promise 状态为 resolve,结果为该值。
  • 若回调返回另一个 promise,新 promise 会 “继承” 其状态和结果。
  • 若回调抛出错误(throw new Error(...)),新 Promise 状态为 rejected,错误为抛出的值。

实例方法

then()

正如前面所说,实例方法then()最多接受两个参数:用于 promise 兑现拒绝情况的回调函数。

它会立即返回一个等效的 promise 对象,允许你链接到其他 promise 方法,从而实现链式调用

注:

  • then() 方法用于为 promise 对象的敲定设置回调函数。它是 Promise 的基本方法:thenable规范要求所有类 Promise 对象都提供一个 then() 方法,并且 catch()finally() 方法都会通过调用该对象的 then() 方法来工作。

  • then() 方法返回一个新的 promise 对象。如果在同一 promise 对象上两次调用 then() 方法(而不是链式调用),则该 promise 对象将具有两对处理方法附加到同一 promise 对象的所有处理方法总是按照它们添加的顺序调用。

  • 每次独立调用 then() 方法返回 promise 对象开始了独立的链,不会等待彼此的敲定

catch()

实例 catch() 方法用于注册一个在 promise拒绝时调用的函数。

catch(reject)then(undefined,reject)的简写形式,两者等价。

finally()

实例 finally() 方法用于注册一个在 promise 敲定时调用的函数。这可以让你避免在 promisethen()catch()处理器中编写重复代码。

例:

function checkMail() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      resolve("Mail has arrived");
    } else {
      reject(new Error("Failed to arrive"));
    }
  });
}

checkMail()
  .then((mail) => {
    console.log(mail);
  })
  .catch((err) => {
    console.error(err);
  })
  .finally(() => {
    console.log("Experiment completed");
  });
 //Error: Failed to arrive
 //"Experiment completed"

注:在 finally 回调函数中抛出错误(或返回被拒绝promise)仍会导致当前的 promise拒绝

静态方法

resolve()

resolve(value)方法以给定值“解决(resolve)”一个 promise,返回一个状态为 resolvepromise对象。

  • value是非thenable对象,返回一个以该值兑现promise

  • value是一个 thenable对象,Promise.resolve() 将其 then() 方法封装为任务推入微队列

  • value本身就是一个 promise,那么该 promise 将被返回。

该方法将嵌套的类 promise 对象(例如,一个将被兑现为另一个 promise 对象的 promise 对象)展平,转化为单个 promise 对象,其兑现值为一个非 thenable 值。

传入普通参数

Promise.resolve("成功").then(
  (value) => {
    console.log(value); // "成功"
  },
  (reason) => {
    // 不会被调用
  },
);

传入promise对象参数

Promise.resolve() 方法传入一个promise参数时,会重用已存在的 Promise 实例。它将返回同一 Promise 实例,而不会创建一个新封装对象

const original = Promise.resolve(33);
const cast = Promise.resolve(original);
cast.then((value) => {
  console.log(`值:${value}`);  //值:33
});
console.log(`original === cast ? ${original === cast}`);  //original === cast ? true

传入 thenable 对象参数

// Thenable 在成功回调之前抛出异常
// Promise 被拒绝
const thenable = {
  then(onFulfilled) {
    throw new TypeError("抛出异常");
    onFulfilled("Resolving");
  },
};

const p2 = Promise.resolve(thenable);
p2.then(
  (v) => {
    // 不会被调用
  },
  (e) => {
    console.error(e); // TypeError: 抛出异常
  },
);
// Thenable 在成功回调之后抛出异常
// Promise被解决
const thenable = {
  then(onFulfilled) {
    onFulfilled("解决");
    throw new TypeError("Throwing");
  },
};

const p3 = Promise.resolve(thenable);
p3.then(
  (v) => {
    console.log(v); // "解决"
  },
  (e) => {
    // 不会被调用
  },
);
reject()

**Promise.reject(error)**方法返回状态为 rejectedpromise 对象,错误为 error

function resolved(result) {
  console.log("Resolved");
}

function rejected(result) {
  console.error(result);
}

Promise.reject(new Error("fail")).then(resolved, rejected);
// Expected output: Error: fail
try()

该静态方法接受一个任意类型的回调函数(无论其是同步或异步,返回数据或抛出异常),并将其结果封装成一个 promise

Promise.try(func)
Promise.try(func, ..arg)

参数

  • func:使用提供的参数(arg1arg2、…、argN同步调用的函数。它可以做任何事情——要么返回一个值、抛出一个错误或者返回一个 promise
  • ..arg:传入给 func 的参数列表。
all()any()

all()

Promise.all() 静态方法接受一个 Promise 可迭代对象iterable(具有foreach()方法可枚举元素,如数组)作为输入,并返回一个 Promise

  • 当输入的所有 promise 都被兑现(包括传递了空的可迭代对象),返回的 promise也将被兑现,并带有包含所有兑现的数组。如果 iterable 包含非 promise 值,这些值将被忽略,但仍包含在携带的 promise兑现数组中。
  • 如果输入的iterable中的 promise 任何一个被拒绝,则返回的 promise 将被拒绝,并带有第一个被拒绝的原因。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// Expected output: Array [3, 42, "foo"]

any()

Promise.any() 静态方法接受一个 promise 可迭代对象iterable(具有foreach()方法可枚举元素,如数组)作为输入,并返回一个 Promise

  • 当输入的iterable中的 promise任何一个被兑现,这个返回的 promise 将会兑现,并带有第一个兑现的值
  • 当输入的所有 promise 都被拒绝(包括传递了空的可迭代对象),并带有包含所有拒绝原因的数组。如果 iterable 包含非 promise 值,这些值将被忽略,但仍包含在携带的 promise拒绝原因数组中。
const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick"));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow"));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then((value) => console.log(value));

// Expected output: "quick"
allSettled()

Promise.allSettled() 静态方法接收 promise 可迭代对象iterable(包括传入空的可迭代对象时),并返回一个单独的 promise。当所有输入的 Promise 都已敲定,返回的 promise 将被兑现,并带有描述每个 promise 结果的对象数组。

**allSettled()**与all()的最大区别就是:allSettled()不管兑现拒绝与否,都带有描述每个promise执行结果的对象数组,这意味着我们可掌握每个异步操作的执行情况,根据执行情况进行后续更灵活的操作(如:即使部分操作失败,也保留成功的结果)。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 100, "foo"),
);
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) =>
  results.forEach((result) => console.log(result.status)),
);

// Expected output:
// "fulfilled"
// "rejected"

兑现携带的results数组中的元素对象都含有一下属性:

  • status:一个字符串,要么是 "fulfilled",要么是 "rejected",表示 promise 的最终状态。
  • value:仅当 status"fulfilled",才存在,promise 兑现的值。
  • reason:仅当 status"rejected",才存在,promsie 拒绝的原因。

async/await

async/awaitJavaScript 中基于 Promise语法糖,它让异步代码的写法更接近同步代码,进一步简化了异步逻辑的处理。可以理解为:async/await 是 Promise 的 “语法升级”,完全依赖 Promise 工作

async/await做了哪些升级?

痛点:开发者需要的异步操作往往是一个函数,传统的Promise编程总是需要将异步操作封装进一个promise对象中,这编写了很多单调而重复的代码。

解决:使用async可以直接将一个函数标记为一个异步操作,底层会自动封装成promise对象。

痛点:为了实现Promise的链式编程,Promise的每个实例方法或静态方法都会返回一个promise对象,对开发者而言,更关心的是兑现值或拒绝原因。

解决:使用await可以直接获取返回值那样获取兑现值,被拒绝则直接抛出错误。

async 关键字

  • 用于修饰函数(普通函数、箭头函数、对象方法等),表示该函数返回一个 Promise 对象
  • 函数内部的返回值会被自动包装Promise.resolve(返回值);若抛出错误,则会被包装成 Promise.reject(错误)

示例

// 普通函数
async function fn1() {
  return "成功结果"; // 等价于 return Promise.resolve("成功结果")
}

// 箭头函数
const fn2 = async () => {
  throw new Error("出错了"); // 等价于 return Promise.reject(new Error("出错了"))
};

// 调用 async 函数,返回的是 Promise
fn1().then(result => console.log(result)); // 输出 "成功结果"
fn2().catch(error => console.log(error.message)); // 输出 "出错了"

await 关键字

  • 只能在 async 函数内部使用,用于等待一个 Promise 完成
  • await 后面跟一个 Promise 时,会暂停当前 async 函数的执行,直到该 Promise敲定
  • Promise兑现await返回其兑现结果;若被拒绝,则会拒绝原因封装为错误抛出(可被 try/catch 捕获)。

async/await改造一下前面的模拟获取用户信息->获取用户订单->获取用户订单详情的三层异步操作:

// 基于之前的 getUser / getOrders / getOrderDetail(返回 Promise)
async function getResult() {
  try {
    // 等待 getUser 完成,拿到用户信息
    const user = await getUser(1); 
    // 等待 getOrders 完成,拿到订单列表
    const orders = await getOrders(user); 
    // 等待 getOrderDetail 完成,拿到详情
    const detail = await getOrderDetail(orders[0]); 
    console.log("最终结果:", detail); // 输出订单详情
    return detail; // 返回结果会被包装成 Promise
  } catch (error) {
    // 捕获所有 await 过程中可能出现的错误
    console.log("出错了:", error.message);
  }
}

// 调用 async 函数
getResult();

这段代码的逻辑和之前用 then 链式调用完全一致,但写法更像同步代码,可读性大幅提升,使得异步代码和同步代码在书写时几乎没有区别。

注意事项

  • await 只能在 async 函数中使用,否则报错Unexpected token 'await'

  • await 后面可以跟非 Promise 值:此时会直接返回该值,相当于 await Promise.resolve(值)

  • async 函数不会阻塞渲染主线程await 只会暂停当前 async 函数的执行(将后续代码逻辑封装为任务,等待当前Promise敲定后推入微队列),不会阻塞外部代码。

async function delay() {
  console.log("开始等待");
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log("等待结束");
}

delay();
console.log("外部代码先执行"); // 会先输出这句,说明主线程没被阻塞
/*输出:
开始等待
外部代码先执行
(1秒后)等待结束
*/

并行处理的优化

并行处理异步操作:如果多个异步操作互不依赖,不要用 await 逐个等待(底层链式调用,会浪费时间),而应结合 Promise.all() 并行执行。

反例(低效)

async function bad() {
  // 两个操作无依赖,但串行执行,总耗时 = t1 + t2
  const res1 = await fetch("/api1");
  const res2 = await fetch("/api2"); 
}

正例(高效)

async function good() {
  // 并行执行,总耗时 = max(t1, t2)
  const promise1 = fetch("/api1");  //先开启异步操作1
  const promise2 = fetch("/api2");  //先开启异步操作2
  const res1 = await promise1;  //等待异步操作结果1
  const res2 = await promise2;  //等待异步操作结果2
}

上述两个代码本质是是否先启动所有异步操作再等待结果, 这是日常开发中优化异步性能的关键技巧。

当需要等待多个互不依赖的异步操作结果时,使用Promise.all()是更优雅的操作:

async function better() {
  // 同时启动所有请求,等待全部完成后,按顺序返回结果数组
  const [res1, res2] = await Promise.all([
    fetch("/api1"),
    fetch("/api2")
  ]);
  console.log(res1); // 对应第一个 Promise 的结果
  console.log(res2); // 对应第二个 Promise 的结果
}

Promise.all()需要其中所有的异步操作都兑现时才执行后续代码。

进阶:有时需要==「即使部分请求失败,也保留成功的结果」==,可以用 Promise.allSettled()(只要都敲定即可执行后续代码):

async function handlePartialFail() {
  const results = await Promise.allSettled([
    fetch("/api1"),
    fetch("/api2")
  ]);
  // 遍历结果,过滤出成功的请求
  const successResults = results
    .filter(result => result.status === "fulfilled")
    .map(result => result.value);
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐