Promise中你可能忽略的点

状态

一个 promise 必须处于三种状态之一:pending(待定 )、fulfilled(已兑现 )或 rejected(已拒绝

如果一个 Promise 已经被兑现或拒绝,即不再处于待定状态,那么则称之为已敲定(settled) 也就是说fulfilledrejected 统称为已敲定(settled

而且promise一旦敲定(settled)之后,则该promise的状态将不会再变化。即只有pending状态的promise才可以进行状态变化。

初始化

语法:

js 复制代码
new Promise(executor)

function executor(resolveFunc, rejectFunc) {
  // 通常,`executor` 函数用于封装某些接受回调函数作为参数的异步操作
}
  1. executor 是同步调用的(在构造 Promise 时立即调用),并将 resolveFuncrejectFunc 函数作为传入参数。这个在事件循环判断执行顺序的时候要记住这点

  2. 如果 executor 抛出错误,则 Promise 被拒绝(rejected)。但是,如果 resolveFunc 或 rejectFunc 中的一个已经被调用(此Promise已经被解决),则忽略该错误

如上图,我们直接在executor函数中抛出错误,可以看到promise的状态是rejected

如果在抛出异常之前调用resolveFunc:

可以看到,确实是忽略了下面的throw语句,promise状态为fulfilled

如果先抛出异常,后resolve呢?

发现跟直接抛出错误是一样的效果,那是因为throw和return类似,都会中断程序,也就不会往下执行

链式调用

Promise.prototype.then()Promise.prototype.catch()Promise.prototype.finally() 方法用于将进一步的操作与已敲定的 Promise 相关联。由于这些方法返回 Promise,因此它们可以被链式调用。

也就是只有当前面的Promise敲定(settled)之后 才可能会调用后面.then,.catch,.finally方法中相应的回调函数(具体的调用时机还要看当前的调用栈和事件队列)。

.then() 方法最多接受两个参数,第一个参数是Promise兑现(fulfilled)时的回调函数,第二个参数是该promise拒绝时(rejected)时的回调函数。

js 复制代码
then(onFulfilled)
then(onFulfilled, onRejected)

如果 onFulfilled 不是一个函数,则内部会被替换为一个恒等 函数((x) => x),它只是简单地将兑现值向前传

也就是 .then(2)等效于.then((x)=>x),传入的对象会被忽略,但仍然会返回一个新的promise对象

如果 onRejected 不是一个函数,则内部会被替换为一个抛出器 函数((x) => { throw x; }),它会抛出它收到的拒绝原因

可以看到我们可以在每个then方法的onRejected函数处理错误,不过在实际开发中一般都会在链式调用的最后调用.catch方法统一处理错误。

并且.then()方法会立即返回一个新的Promise,且返回的新Promise的状态一定是pending, 无论当前 Promise 对象的状态如何。

js 复制代码
const p = new Promise((resolve)=> {
    resolve('1')
})
const p2 = p.then((value) => {
    console.log(value)
})
console.log(p === p2) // false
console.log(p2) // Promise {<pending>}

为什么是pending状态?这是因为then方法的回调函数会被放到微任务队列异步执行,在下一次事件循环时才可能被放置在调用栈执行。感兴趣的可以看下这篇事件循环

也就是传入 then() 的函数永远不会被同步调用,即使 Promise 已经被解决了(resolved)。所以应该尽可能的将多个同步操作放到一个then方法里完成 ,而不是将多个同步操作放到不同的.then方法里。

.then方法返回的 Promise 对象(称之为 p)的行为取决于处理函数(onFulfilledonRejected)的执行结果,遵循一组特定的规则

  • 返回一个值:p 以该返回值作为其兑现值。
  • 没有返回任何值:pundefined 作为其兑现值。
  • 抛出一个错误:p 抛出的错误作为其拒绝值。
  • 返回一个已兑现的 Promise 对象:p 以该 Promise 的值作为其兑现值。
  • 返回一个已拒绝的 Promise 对象:p 以该 Promise 的值作为其拒绝值。
  • 返回另一个待定的 Promise 对象:p 保持待定(pending)状态,并在该 Promise 对象被兑现/拒绝后立即以该 Promise 的值作为其兑现/拒绝值

catch() 方法内部会调用当前 promise 对象的 then() 方法,并将 undefinedonRejected 作为参数传递给 then()

finally() 方法类似于调用 then(onFinally, onFinally)。然而,有几个不同之处:

  • 创建内联函数时,你可以只将其传入一次,而不是强制声明两次或为其创建变量。

  • onFinally 回调函数不接收任何参数。这种情况恰好适用于你不关心拒绝原因或兑现值的情况,因此无需提供它。

  • finally() 调用通常是透明的,不会更改原始 promise 的状态。例如:

    • Promise.resolve(2).then(() => 77, () => {}) 不同,它返回一个最终会兑现为值 77 的 promise,而 Promise.resolve(2).finally(() => 77) 返回一个最终兑现为值 2 的 promise。
    • 类似地,与 Promise.reject(3).then(() => {}, () => 88) 不同,它返回一个最终兑现为值 88 的 promise,而 Promise.reject(3).finally(() => 88) 返回一个最终以原因 3 拒绝的 promise

除了上面的方法,Promise.resolve() 静态方法也是常用的方法之一,语法:

js 复制代码
Promise.resolve(value)

value值可以是:

  • Promise对象,将返回该Promise对象

    js 复制代码
    const p = new Promise((resolve) => {
    resolve('1')
    })
    const p2 = Promise.resolve(p)
    console.log(p === p2) // true

    也就是会返回同一个Promise对象,而不是创建一个封装对象

  • thenable 对象,Promise.resolve() 将调用其 then() 方法及其两个回调函数

  • 其他值:直接以value兑现

async和await

async/await 简化了Promise api的使用。

async

语法:

js 复制代码
async function name(param0) {
  statements
}
async function name(param0, param1) {
  statements
}
async function name(param0, param1, /* ..., */ paramN) {
  statements
}

异步函数总是返回一个 promise。如果一个异步函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。

js 复制代码
async function foo() {
  return 1;
}

function foo() {
  return Promise.resolve(1);
}

两种方式比较类似,但也有不一样的点,如果返回的是promise,则async函数会返回一个不同的引用,Promise.resolve则会返回相同的引用。

js 复制代码
const p = new Promise((res, rej) => {
  res(1);
});

async function asyncReturn() {
  return p;
}

function basicReturn() {
  return Promise.resolve(p);
}

console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false

异步函数的函数体可以被看作是由零个或者多个 await 表达式分割开来的。从顶层代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行 的。因此,不包含 await 表达式的异步函数是同步运行的 。然而,如果函数体内包含 await 表达式,则异步函数就一定会异步完成

javascript 复制代码
async function foo() {
  await 1;
}
// 等价于
function foo() {
  return Promise.resolve(1).then(() => undefined);
}

await

语法:

js 复制代码
await expression;

expression是要等待的Promise实例,Thenable对象或者任意类型的值。返回从 Promise 实例或 thenable 对象取得的处理结果。如果等待的值不符合 thenable,则返回表达式本身的值。

await可以拆开Promise的包装,获取其兑现值:await会暂停当前异步函数的执行,在该Promise敲定(settled,兑现或拒绝)之后继续执行。函数执行恢复时,await表达式的值就变成了Promise的兑现值。

若该 Promise 被拒绝(rejected),await 表达式会把拒绝的原因(reason)抛出。当前函数(await 所在的函数)会出现在抛出的错误的栈追踪(stack trace),否则当前函数就不会在栈追踪出现

js 复制代码
function resolveAfter2Seconds(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function f1() {
  let x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}

f1();

可以看到,await的值就是Promise的兑现值,也就是resolve函数的值。

await 对执行过程的影响

当函数执行到 await 时,被等待的表达式会立即执行,所有依赖该表达式的值的代码会被暂停,并推送进微任务队列(microtask queue)。然后主线程被释放出来,用于事件循环中的下一个任务。即使等待的值是已经敲定的 promise 或不是 promise,也会发生这种情况

js 复制代码
async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

也就是await后面紧跟的表达式( await console.log(name, "middle"))会被同步的执行await下面的语句(console.log(name, "end"))会被放置到微任务队列异步执行 。如果await后面紧跟的是另外一个异步函数,也会同步的执行直到遇到await

对应的Promise写法:

js 复制代码
function foo(name) {
  return new Promise((resolve) => {
    console.log(name, "start");
    resolve(console.log(name, "middle"));
  }).then(() => {
    console.log(name, "end");
  });
}
栈追踪

有时,当异步函数直接返回一个 Promise 时我们会省略 await

js 复制代码
async function noAwait() {
  // Some actions...

  return /* await */ lastAsyncTask();
}

其实加不加await,函数noAwait的兑现值都是一致的。那为什么会建议省略await?或者加上await之后会有什么不一样的?

以上面的函数为例,没有await时,noAwait 同步执行完lastAsyncTask函数之后就执行完毕了,noAwait被pop出调用栈。

如果加上await,那么noAwait就会等待lastAsyncTask异步函数兑现之后才能结束。代码就会变成类似于:

js 复制代码
async function noAwait() {
  // Some actions...

  const res = await lastAsyncTask();
  return res;
}

也就是当执行 lastAsyncTask 函数的时候,如果没有await,当前调用栈只有lastAsyncTask,如果有await,noAwait,lastAsyncTask都会出现在调用栈,且lastAsyncTask优先出栈。

这种区别在错误追踪的时候会有更好的体现,比如lastAsyncTask函数抛出一个错误时:

js 复制代码
async function lastAsyncTask() {
  await null;
  throw new Error("failed");
}

async function noAwait() {
  return lastAsyncTask();
}

async function withAwait() {
  return await lastAsyncTask();
}

noAwait函数的栈追踪为:

withAwait的栈追踪为:

可以看到,使用await可以得到更全面的栈追踪信息,但是,这样会有一点性能牺牲,毕竟 Promise 会被拆装了又再次包装

文章的内容都出自下面的链接并加以自己的理解,建议大家都能看下原文

参考文档

  1. developer.mozilla.org/zh-CN/docs/...
  2. developer.mozilla.org/zh-CN/docs/...
  3. developer.mozilla.org/zh-CN/docs/...
  4. developer.mozilla.org/zh-CN/docs/...
相关推荐
掘了20 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅20 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅20 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅21 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment21 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅21 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊21 小时前
jwt介绍
前端
爱敲代码的小鱼21 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte21 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT061 天前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法