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/...
相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀3 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef5 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js