Skip to content

一文搞懂 async,await,.then 的链式调用

1.async 和 await

async

该关键字是放在函数之前的,使得函数成为一个异步函数,他最大的特点就是将函数封装成 Promise,也就是被他修饰的函数的返回值都是 Promise 对象。而这个 Promise 对象的状态则是由函数执行的返回值决定的。

如果返回的是一个非 promise 对象(同样会被包裹为 promise),该函数将返回一个成功的 Promise,成功的值则是返回的值;

如果返回的是一个 promise 对象,则该函数返回的就是该 promise 对应的状态。

——> 这个特点其实和 await 没什么关系,和.then的返回值包装规则比较类似。

await

await 右边是一个表达式,如果该表达式(异步函数)返回的是一个 Promise 对象,则左边接收的结果就是该 Promise 对象成功的结果,如果该 Promise 对象失败了,就必须使用 try..catch 来捕获。如果该表达式返回的不是一个 promise 对象,则左边接收的就是该表达式的返回值。

当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面(async 关键字包裹的函数里面)才起作用。它可以放在任何异步的、基于 promise 的函数之前。它会暂停异步函数的代码在该行上,直到 promise 完成,然后返回结果值(将异步执行变为了同步)。

在暂停的同时,在外层异步函数所处在的外层同步代码中其他正在等待执行的代码就有机会执行了。

也就是下面这个说法:

当使用 await 表达式时,异步函数会在遇到异步操作时暂停执行,并允许其他代码继续执行

——> 这句话的解释?

其实也就是我们常说的,async+await 的组合让异步代码看起来像同步代码!但是实际上本来就是要用同步套异步,这里我们变成了异步套异步,但是外层的异步让我们用 await 变成同步了,所以本质上异步同步没有区别,只是写法会更加优雅易懂!

作用:这样可以实现非阻塞的异步操作,让程序能够同时处理多个任务,提高效率和响应性。

以下是一个示例,展示了使用 await 实现非阻塞异步操作的情况:

js
async function fetchData() {
  console.log("Start fetching data...");
  const response = await fetch("https://api.example.com/data"); // 等待异步网络请求完成
  const data = await response.json(); // 等待将响应转换为 JSON
  console.log("Data fetched:", data);
}

console.log("Before calling fetchData");
fetchData(); //因为fetchData这个异步函数里面有 await 关键字,才让这个异步函数可以不被等待就向下执行
console.log("After calling fetchData");

在上面的示例中,我们定义了一个异步函数 fetchData,它使用 await 表达式来等待异步网络请求的完成和响应的解析。

在调用 fetchData 函数之前和之后,我们分别输出了两行日志。这样可以观察到异步函数的执行是非阻塞的,它在遇到 await 表达式时会暂停执行,让其他代码继续执行。

当我们运行上述代码时,输出的日志顺序如下:

kotlin
Before calling fetchData
Start fetching data...
After calling fetchData //继续执行了同步代码
Data fetched: [data] //异步网络请求完成

可以看到,调用 fetchData 函数时,它会立即执行到第一个 await 表达式处,并暂停执行。然后,控制权返回给调用者,继续执行后续的代码,即输出 "After calling fetchData"。

在后台,异步网络请求在执行,直到完成。一旦网络请求完成,异步函数会恢复执行,继续执行后续的代码,并输出 "Data fetched: [data]"。

关于 await 的总结:

如果在异步函数中没有使用 await 表达式,调用者会等待异步函数执行完毕,但它不需要等待具体的异步操作完成,而是等待整个异步函数执行完毕后获取最终的返回值。

作用:使用了await表达式,那么调用者就不需要等待异步函数执行完毕(让外层的异步变为不会阻塞的函数),而异步函数内部需要等待具体的异步操作完成(把内部的异步变为了同步)。

其实只要使用了 await,就至少有两层异步函数,被 async 包裹的函数本身就是一个大的异步函数(async 对于 await 来说的主要价值就是会把函数变为异步的),而 await 也是加在一个返回 promise 对象的异步函数上的(promise 本身就是用于将异步操作包装成一个对象,异步操作可以更加可控和易于处理),而 await 的主要作用就是把异步变为同步(以同步的方式编写异步代码,不用再.then 了),但是要发挥 await 的这个作用的代价就是要把 await 放在一个 async 异步函数里面,也就是 async 异步套 await 异步

异步变同步的主要作用是简化 promise 状态流转(.then)的编写复杂度和样式;让异步代码看起来是同步的(实际上也变成同步的了),更易于逻辑的构建与编写(有些操作需要特定的先天条件,如 echarts 渲染需要数据)

总之,通过使用 "async" 和 "await",我们可以以同步的方式编写异步代码(直接把异步函数的 promise 当做同步的来操作),使其更易于阅读和理解。

举个 async+await 的应用例子:

jsx
f1 = () => {
  return new Promise((resolve, reject) => {
    // resolve(1);
    reject("错误");
  });
};

async function test() {
  try {
    const p = await f1();
    console.log(p);
  } catch (error) {
    console.error(error);
  }
}
test();

promise 的注意事项:

Promise 是 JavaScript 提供的一种用于处理异步操作的机制。它可以用于将异步操作包装成一个对象,使得异步操作可以更加可控和易于处理。

Promise 对象表示一个异步操作的最终完成或失败,并可以返回异步操作的结果或错误信息

但是new Promise((resolve, reject) => { ... }) 中的代码不一定是异步函数,但通常用于处理异步操作。也就是说 Promise 也不是一定用来处理异步操作的!

new Promise 的构造函数中,我们可以执行任意代码逻辑,包括同步操作和异步操作。一般情况下,我们在 Promise 构造函数中执行的是异步操作,例如进行网络请求、读取文件、定时器等等。

使用 Promise 的主要目的是为了处理异步操作的结果,并将其封装成一个可处理的对象,以便后续进行链式操作或使用 .then.catch 方法处理结果和错误。

2. .then 的包装规则以及链式调用法则

在 Promise 的链式调用中,.then() 方法的返回值会影响后续的 Promise 状态和值。下面是一些关于 .then() 返回值的规则:

  1. 如果 .then() 方法中的回调函数返回一个值(非 Promise 对象),则返回的 Promise 对象将会处于已解决(fulfilled)状态,并且该值将作为解决值传递给下一个 .then() 方法的回调函数。例如:

    js
    someAsyncFunction()
      .then((result) => {
        return result * 2; // 返回一个值
      })
      .then((finalResult) => {
        console.log(finalResult); // 使用上一个回调函数的返回值
      });
  2. 如果 .then() 方法中的回调函数返回一个 Promise 对象,则返回的 Promise 对象的状态和值将取决于该返回的 Promise 对象。如果返回的 Promise 对象被解决(fulfilled),则后续的 .then() 方法将接收到解决值;如果返回的 Promise 对象被拒绝(rejected),则后续的 .catch() 或带有拒绝回调的 .then() 方法将被触发。例如:

    js
    someAsyncFunction()
      .then((result) => {
        return new Promise((resolve, reject) => {
          resolve(result * 2); // 返回一个 Promise 对象
        });
      })
      .then((finalResult) => {
        console.log(finalResult); // 使用上一个回调函数返回的 Promise 的解决值
      });

    注意:使用 resolve 方法将会返回一个已解决(fulfilled)状态的 Promise 对象,而使用 reject 方法将会返回一个已拒绝(rejected)状态的 Promise 对象。

    • resolve("Promise resolved");

    • reject(new Error("Promise rejected"));

  3. 如果 .then() 方法中的回调函数抛出一个异常,则返回的 Promise 对象将会处于被拒绝(rejected)状态,并且异常将作为拒绝理由传递给后续的 .catch() 或带有拒绝回调的 .then() 方法。例如:

    js
    someAsyncFunction()
      .then((result) => {
        throw new Error("Something went wrong"); // 抛出一个异常
      })
      .catch((error) => {
        console.log(error); // 捕获上一个回调函数抛出的异常
      });

需要注意的是,.then() 方法返回的 Promise 对象是一个全新的 Promise 对象,它与前一个 Promise 对象是不同的。因此,在 Promise 链中可以进行多个 .then() 方法的串联调用,每个 .then() 方法都可以进行值的转换、异步操作或错误处理。

如果没有返回值呢,也没有使用 resolve 和 reject?还会继续链式调用吗?

如果前一个.then()没有返回值(即没有明确的 return 语句),后续的.then() 仍然会被调用,但它们会接收到前一个.then()中的 undefined 作为参数

考虑以下示例:

js
promiseFunction()
  .then((result) => {
    console.log("First .then():", result); // 输出结果:First .then(): undefined
  })
  .then((result) => {
    console.log("Second .then():", result); // 输出结果:Second .then(): undefined
  });

在这个示例中,如果 promiseFunction() 返回一个 Promise,并且在第一个.then()中没有明确的返回值,那么第二个.then() 会被调用,但它会收到 undefined 作为参数。如果在第一个.then()中有返回值,那么第二个.then() 将会收到这个返回值作为参数。

那么 promise 的状态会是什么?

如果在 .then() 中没有返回值(或者显式地返回了 undefined),Promise 的状态仍然会保持为已解决(fulfilled)状态,但是 Promise 内部的值会变成 undefined

这是因为在 JavaScript 的 Promise 链中,如果 .then().catch() 中没有返回任何值,那么会隐式地返回一个 Promise,该 Promise 的状态为已解决(fulfilled)状态,并且值为 undefined

总结:如果在 Promise 链的 .then() 中没有明确的返回值,Promise 的状态仍然是已解决(fulfilled),但内部的值会变成 undefined

为什么非要隐式地返回一个 fulfilled 状态的 undefined 呢?

这是因为,只有当 Promise 对象被解决(fulfilled)时,才会触发该 Promise 对象对应的 .then() 方法中的回调函数,Promise 的每个状态对应着不同的处理方法(.then、.catch),每个状态是通过特定的状态流转方法(resolve、reject)得来的,有着严格的对应关系!

如果连续的链式.then 时,其中有一个.then 在执行过程中发生了错误,那么后面的.then 还会 执行吗?

如果在连续的链式 .then() 方法中的某个回调函数发生了错误(抛出异常),则后续的 .then() 方法会被跳过,直接进入最近的 .catch() 方法(如果有的话)来处理错误。

这是因为在 Promise 链式调用中,每个 .then() 方法返回的是一个新的 Promise 对象,用于处理前一个 最近的状态符合的 Promise 对象的解决值。如果前一个 .then() 方法中的回调函数发生错误,它会导致该 Promise 对象变为已拒绝状态,而后续的 .then() 方法会被跳过。

直接 return 和使用resolve 方法产生的效果是基本一样的

在某种程度上可以说返回一个值和使用 resolve 方法的效果是类似的,因为它们都会将一个值作为 Promise 对象的解决值进行传递,并且都是 fulfilled 状态的。

在大多数情况下,使用隐式的 return 语句是更常见和推荐的方式,因为它更简洁和直观。而手动创建 Promise 对象并使用 resolve 方法则更适用于需要进行更复杂操作或需要手动处理异步操作的情况。

注意:即便没有任何 return,也会隐式地返回一个 fulfilled 状态的值为 undefined 的 Promise 对象!

一个使用 redux-promise 中间件解决异步 action 时的例子:

js
export const incrementAsyncAction = (data, delay) => {
  return axios({ url: xxx, method: "get", headers: {} }).then((res) => {
    return incrementAction(res.data); //内部返回一个同步 action 对象,会被封装为一个 Promise 对象,并且状态为fulfilled(已解决),还可以继续.then
    //如果使用 resolve方法 那么同样返回的是一个fulfilled的Promise对象
  });
};

注意:上述 incrementAsyncAction 方法返回的是一个 fulfilled 的 Promise 对象

Promise 链式调用是基于 Promise 对象的状态和值,而不是依赖于回调函数的返回值

可以这样理解:在 Promise 链式调用中,每个 .then() 方法接收一个回调函数,而返回的是一个新的 Promise 对象,该对象的状态和值取决于前一个 Promise 对象的状态和前一个回调函数的返回值。

回调函数的返回值在 Promise 链中的作用是影响下一个 .then() 方法的输入值(即解决值)以及 Promise 的状态。

注意,当 Promise 对象被解决(fulfilled)时,会触发该 Promise 对象对应的 .then() 方法中的回调函数。而回调函数的返回值(无论有无)并不会直接影响 Promise 对象的状态,无非是不会走不会执行.then()方法罢了!

要正确理解 Promise 链式调用,需要将重点放在 Promise 对象的状态和值的变化上,而回调函数的返回值仅仅是影响下一个 Promise 对象的输入值(解决值)而已。

总结:resolve 和 reject 方法用于状态的流转,then 和 catch 方法用于状态的接收!

.then 的第二个参数,以及.catch 的深入理解

.catch 后面如果还有.then 也是可以被执行的

js
// 函数A只返回一个reject异常
function A() {
  return Promise.reject(new Error(Math.random()));
}

// 这样会先执行catch,然后执行后面的then
A()
  .then(() => console.log("第一个then"))
  .catch((e) => console.error(e))
  .then(() => console.log("第二个then"));

原因:catch 也会隐式地返回一个值为 undefined 的 promise(fulfilled 状态的)所以可以继续 then。 从语义上也很好理解,都被捕获处理过了就不是 error 了,所以可以继续!

如果我想如果报错就捕获 catch,并且不执行后面的 then 的话要怎么做呢?

1.改变顺序使用.then.catch,then 里面要是报错也会被 catch 捕获,把 catch 放到最后

2.使用 try-catch,任意 Promise 中的 catch 异常,都会阻断后面代码的执行,并跳转到 try 中的 catch,被捕获

js
try {
  A()
    .then(() => console.log("第一个then"))
    .then(() => console.log("第二个then"));
} catch (e) {
  console.error(e);
}

如果 catch 后面有 catch,会执行吗?

可能会执行,是有条件的:需要显示地 reject 才可以,因为默认返回的是 fulfilled 状态的对象,只能被 then 捕获!

js
function B() {
  return Promise.resolve("success!");
}
B()
  .then((result) => {
    console.log(result); // 打印前一个回调函数的返回值
  })
  .catch((error) => {
    return "catch Error"; //返回的是值为undefined的fulfilled状态的Promise对象,只能被then捕获!
  })
  .catch((error) => console.log(error.message)); //不会被执行!
image-20230813010532331