JavaScript内核与高级编程之:`Async/Await`:基于`Generator`和`Promise`的语法糖。

各位观众老爷,大家好!我是你们的老朋友,今天咱们聊聊JavaScript里那个甜甜的语法糖:Async/Await

别看它名字挺高大上,其实就是GeneratorPromise这对好基友的马甲,穿上之后立马变得人见人爱,花见花开。咱们今天就扒一扒这件马甲,看看里面藏着啥秘密。

开胃小菜:Promise 温故知新

在进入Async/Await的世界之前,咱们先简单回顾一下PromisePromise这玩意儿,说白了就是一个代表异步操作最终结果的对象。它可以处于三种状态:

  • Pending (进行中): 初始状态,还没完成,像你刚开始追妹子。
  • Fulfilled (已成功): 操作成功完成,就像你成功抱得美人归。
  • Rejected (已失败): 操作失败,就像你表白被拒,惨遭滑铁卢。

Promise最常用的两个方法是.then().catch().then()用于处理成功的情况,.catch()用于处理失败的情况。

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { // 模拟异步请求
      if (userId > 0) {
        resolve({ id: userId, name: "张三", age: 25 }); // 成功
      } else {
        reject("用户ID无效"); // 失败
      }
    }, 1000);
  });
}

fetchUserData(1)
  .then(user => {
    console.log("用户信息:", user);
  })
  .catch(error => {
    console.error("出错了:", error);
  });

fetchUserData(0)
  .then(user => {
    console.log("用户信息:", user); // 不会执行
  })
  .catch(error => {
    console.error("出错了:", error); // 用户ID无效
  });

上面这段代码,fetchUserData函数返回一个Promise,模拟了一个异步请求。如果userId大于0,Promiseresolve,否则会reject.then().catch()分别处理成功和失败的情况。

正餐:Async/Await 闪亮登场

Async/Await是 ES2017 (ES8) 引入的,它让异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。

  • async 关键字: 用于声明一个异步函数。异步函数会默认返回一个Promise对象。
  • await 关键字: 只能在async函数中使用。它会暂停async函数的执行,直到await后面的Promise对象resolvereject

咱们用Async/Await改造一下上面的例子:

async function getUserData(userId) {
  try {
    const user = await fetchUserData(userId); // 等待Promise resolve
    console.log("用户信息:", user);
    return user; // 返回值会被包装成 Promise.resolve(user)
  } catch (error) {
    console.error("出错了:", error);
    throw error; // 抛出的错误会被包装成 Promise.reject(error)
  }
}

getUserData(1); // 调用异步函数

getUserData(0); // 调用异步函数, 会输出错误信息

这段代码看起来是不是更像同步代码了?await fetchUserData(userId) 会暂停 getUserData 函数的执行,直到 fetchUserData 返回的 Promise 对象 resolve。如果 Promise 对象 reject 了,try...catch 语句会捕获到这个错误。

Async/Await 的本质:Generator + Promise

Async/Await 其实是 GeneratorPromise 的语法糖。 编译器会将 Async/Await 代码转换成使用 GeneratorPromise 的代码。

咱们先简单了解一下 GeneratorGenerator 是一种特殊的函数,它可以暂停执行,并在稍后恢复执行。它使用 yield 关键字来暂停执行,并返回一个值。

function* myGenerator() {
  console.log("第一次执行");
  yield 1; // 暂停,返回 1
  console.log("第二次执行");
  yield 2; // 暂停,返回 2
  console.log("第三次执行");
  return 3; // 返回 3,并结束 Generator 函数
}

const gen = myGenerator(); // 返回一个 Generator 对象

console.log(gen.next()); // 第一次执行,输出 "第一次执行",返回 { value: 1, done: false }
console.log(gen.next()); // 第二次执行,输出 "第二次执行",返回 { value: 2, done: false }
console.log(gen.next()); // 第三次执行,输出 "第三次执行",返回 { value: 3, done: true }
console.log(gen.next()); // 继续执行,返回 { value: undefined, done: true }

Generator 函数使用 function* 声明,yield 关键字用于暂停执行并返回一个值。gen.next() 方法用于恢复执行,并返回一个包含 valuedone 属性的对象。value 属性是 yield 返回的值,done 属性表示 Generator 函数是否执行完毕。

现在,咱们用 GeneratorPromise 来模拟 Async/Await 的行为。

function asyncToGenerator(fn) {
  return function () {
    const gen = fn.apply(this, arguments); // 执行 Generator 函数,得到 Generator 对象

    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let generatorResult;
        try {
          generatorResult = gen[key](arg); // 执行 Generator 函数的 next/throw 方法
        } catch (error) {
          return reject(error); // 捕获 Generator 函数中的错误
        }

        const { value, done } = generatorResult;

        if (done) {
          return resolve(value); // Generator 函数执行完毕,resolve Promise
        } else {
          // 如果 value 是 Promise 对象,则等待 Promise 对象 resolve
          Promise.resolve(value).then(
            val => step("next", val), // 成功,继续执行 Generator 函数
            err => step("throw", err)  // 失败,抛出错误
          );
        }
      }

      step("next"); // 启动 Generator 函数
    });
  };
}

const getUserDataGenerator = asyncToGenerator(function* (userId) {
  try {
    const user = yield fetchUserData(userId); // yield 返回 Promise 对象
    console.log("用户信息:", user);
    return user;
  } catch (error) {
    console.error("出错了:", error);
    throw error;
  }
});

getUserDataGenerator(1);
getUserDataGenerator(0);

这个 asyncToGenerator 函数接受一个 Generator 函数作为参数,并返回一个新的函数。这个新的函数会执行 Generator 函数,并返回一个 Promise 对象。step 函数用于递归地执行 Generator 函数的 nextthrow 方法,直到 Generator 函数执行完毕。

这段代码的逻辑和使用 Async/Await 的代码几乎一样,但是它使用了 GeneratorPromise 来实现同样的功能。这就是 Async/Await 的本质:Generator + Promise

Async/Await 的优势

  • 代码更简洁易懂: Async/Await 让异步代码看起来更像同步代码,避免了 Promise 的回调地狱。
  • 错误处理更方便: 可以使用 try...catch 语句来捕获异步操作中的错误。
  • 调试更简单: 可以使用调试器单步执行 Async/Await 代码,就像调试同步代码一样。

Async/Await 的注意事项

  • await 关键字只能在 async 函数中使用。
  • async 函数会默认返回一个 Promise 对象。
  • await 会暂停 async 函数的执行,直到 await 后面的 Promise 对象 resolvereject
  • 可以使用 try...catch 语句来捕获异步操作中的错误。
  • 避免在循环中使用 await,这会导致性能问题。 尽量使用 Promise.all 并发执行。

Async/Await 的应用场景

Async/Await 可以用于各种异步操作,例如:

  • 发起 HTTP 请求: 可以使用 fetchaxios 等库来发起 HTTP 请求。
  • 读取文件: 可以使用 fs 模块来读取文件。
  • 访问数据库: 可以使用 mysqlmongodb 等库来访问数据库。
  • 处理定时器: 可以使用 setTimeoutsetInterval 来处理定时器。

一些高级技巧

  1. 并发执行多个 PromisePromise.allPromise.allSettled

    如果你需要并发执行多个 Promise,可以使用 Promise.allPromise.allSettled

    • Promise.all: 接受一个 Promise 数组作为参数,返回一个新的 Promise。只有当数组中所有的 Promiseresolve 时,新的 Promise 才会 resolve,并返回一个包含所有 Promiseresolve 值的数组。如果数组中任何一个 Promise reject,新的 Promise 就会立即 reject
    async function fetchData() {
      const [user, posts] = await Promise.all([
        fetchUserData(1),
        fetchUserPosts(1)
      ]);
      console.log("用户信息:", user);
      console.log("用户文章:", posts);
    }
    • Promise.allSettled: 接受一个 Promise 数组作为参数,返回一个新的 Promise。当数组中所有的 Promise 都完成(resolvereject)时,新的 Promise 才会 resolve,并返回一个包含所有 Promise 结果的数组。数组中的每个结果对象都包含 statusvaluereason 属性。
    async function fetchData() {
      const results = await Promise.allSettled([
        fetchUserData(1),
        fetchUserPosts(0) // 假设这个请求会失败
      ]);
    
      results.forEach(result => {
        if (result.status === "fulfilled") {
          console.log("成功:", result.value);
        } else {
          console.error("失败:", result.reason);
        }
      });
    }
  2. 处理 Async/Await 中的错误:更优雅的错误处理

    除了使用 try...catch 语句,还可以使用第三方库(例如 async-await-error-handling)来更优雅地处理 Async/Await 中的错误。

    // 假设你已经安装了 async-await-error-handling 库
    import { to } from 'async-await-error-handling';
    
    async function fetchData() {
      const [error, user] = await to(fetchUserData(1));
      if (error) {
        console.error("出错了:", error);
        return;
      }
      console.log("用户信息:", user);
    }

    to 函数会捕获 Promisereject,并将其作为第一个参数返回。如果 Promise resolve,第一个参数为 null

对比表格

为了更清晰地对比 PromiseAsync/Await,咱们来个表格:

特性 Promise Async/Await
语法 .then().catch() 回调 asyncawait 关键字
可读性 相对复杂,容易形成回调地狱 更简洁易懂,更像同步代码
错误处理 需要在每个 .catch() 中处理错误 可以使用 try...catch 语句统一处理错误
本质 基于状态机的异步编程模型 GeneratorPromise 的语法糖
使用场景 适用于各种异步操作,但代码结构相对复杂 适用于各种异步操作,代码结构更清晰简洁
调试 调试相对困难,需要理解 Promise 的状态变化 调试更简单,可以像调试同步代码一样单步执行

总结

Async/Await 是 JavaScript 中一个非常强大的特性,它可以让异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。虽然它本质上是 GeneratorPromise 的语法糖,但它提供了一种更优雅、更简洁的异步编程方式。掌握 Async/Await,你就可以轻松驾驭 JavaScript 的异步世界,写出更加高效、更加易懂的代码。

今天的分享就到这里,希望对大家有所帮助!如果有什么问题,欢迎在评论区留言,咱们一起探讨。

最后,记住一点:技术是死的,人是活的。理解了 Async/Await 的本质,才能更好地运用它,写出高质量的代码。

感谢各位的观看! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注