写点什么

ES6 新特性详解 - 异步函数

作者:yuanyxh
  • 2024-09-14
    中国香港
  • 本文字数:2524 字

    阅读完需:约 8 分钟

前言

在学习 JavaScript 的过程中,理解并灵活运用异步相关知识是一件不容易的事情,这体现在代码可读性、健壮性上,好在 ES6 出现后挽回了这一局面,我们不再需要编写可读性不高的回调嵌套,也不用为了代码的健壮性而处处小心,这得益于 Promiseasync Function,它们给我们带来了更优秀的异步方案,今天我们就来学习异步函数相关知识。


学习异步函数前,要求读者熟悉 Promise 与 生成器,如果未了解过,可以看看读者的文章


async Function

虽然 Promise 对于传统的异步编程而言已经足够优秀,但精益求精的前辈们仍觉不足,Promise 的代码执行顺序对于开发者而言依然不够直观,为次,ES6 又提出了异步函数的相关概念。


异步函数,即一个特殊函数,通过在函数声明前添加 async 关键字标识


async function () {}
// or
const asyncArrow = async () => {}
复制代码


在异步函数中,能够使用关键词 await,它期待一个 Promise 值,并等待它决议,此时会暂停当前的程序并返回,控制权交还给主线程去执行其他任务


async function asyncFun() {  await Promise.resolve();
console.log('end');}
asyncFun();console.log('start');
复制代码


上述代码中的正确执行顺序是 startend,这意味着函数调用后异步函数内的程序被暂停了,所以首先输出 start,然后输出 end,它的行为如下


  1. 调用异步函数

  2. 等待一个立即完成的 Promise

  3. 给立即完成的 Promise 添加一个微任务

  4. 返回

  5. 输出 start

  6. 微任务执行,asyncFun 程序恢复执行


await 虽然期待一个 Promise 值,但这不是必须的,它可以等待任何值,此时浏览器会将这个值包装为一个 Promise


async function asyncFun() {  await 1;
// 类似于 await Promise.resolve(1);}
复制代码


await 还能够获取到 Promise 的完成值


async function asyncFun() {  const result = await Promise.resolve('fulfilled');
console.log(result);}
asyncFun(); // fulfilled
复制代码


如果等待的 Promise 被拒绝,那么就会在程序停止的位置抛出异常,我们能够通过 try...catch 进行异常捕获


async function asyncFun() {  try {    await Promise.reject('出错了');  } catch (err) {    console.log(err); // 出错了  }}
asyncFun();
复制代码


异步函数的返回值会被包装成一个 Promise,如果没有显式的返回一个值,那么 Promise 完成值就是隐式返回的 undefined


async function asyncFun() {  return 'complete';}
asyncFun().then((value) => { console.log(value); // complete});
复制代码


看到这里,其实异步函数的一些特性就讲完了,但是你有没有发现异步函数与生成器有很多相像的地方呢?


// asyncasync function asyncFun() {  await Promise.resolve();}
// generatorfunction* generator() { yield Promise.resolve();}
复制代码


异步函数通过 async 来标识,而生成器通过在 function 关键字后添加 * 来标识,且它们都有各自的关键字能用于暂停程序的执行,但仅仅如此吗?看一下下面的代码


function* generator() {  const result = yield Promise.resolve('generator');
console.log(result); // generator}
function run(exec) { // 假设 exec 必定是一个生成器 // 获取生成器对象 const g = exec();
// 启动生成器,获取 yield、return 出的值 const p = g.next();
// 假设 p.value 必定是一个 Promise p.value.then( (value) => g.next(value), (err) => g.throw(err) );}
run(generator);
复制代码


上述代码中,我们添加了一个 run 函数,用于控制生成器的执行,run 函数利用 yield 双向数据传递的特性,接受生成器返回的值,这里我们假定为 Promise 值,并给它添加一个 then 处理回调,当这个 Promise 完成时在成功回调中调用生成器对象的 next 方法并将完成值注入到上一个导致程序暂停的 yield 身上,如果失败则调用 throw 方法并注入失败原因。


现在你发现了吗?其实异步函数只是一个语法糖,基于 Promise + 生成器我们也能实现完全相同的功能,所缺的只是一个控制生成器执行流程的执行器而已。

实现执行器

我们已经了解了异步函数其实只是 Promise + 生成器的语法糖,但还缺少了一个关键的执行器函数,以下是摘抄自 《你不知道的 JavaScript》 的实现


function run(exec, ...args) {  // 调用生成器函数并传递参数  const it = exec.apply(this, args);
return Promise.resolve().then(function handleNext(value) { // 不断处理 next 调用 const next = it.next(value); // 获取 next 返回值
// 立即执行函数,传递 next 返回值 return (function handleResult(next) { if (next.done) { // 执行完毕直接返回 return next.value; } else { // 未执行完毕则给 yield 出的 Promise 添加then处理回调 // 同时避免 yield 非 Promise 值,需要进行一层 resolve 包装 return Promise.resolve(next.value).then( handleNext, // 成功回调,继续调用 handleNext 获取下一个值 function handleErr(err) { // 失败回调 return Promise.resolve( it.throw(err) // 注入异常,如果异常在生成器内部被处理则继续调用 handleResult ).then(handleResult); } ); } })(next); });}
复制代码


拥有了这个执行器函数,我们就能够基于 Promise + 生成器来模拟异步函数的行为了


function* generator() {  try {    const result = yield Promise.reject('出错啦');  } catch (err) {    console.log(err); // 出错啦  }
const data = yield Promise.resolve('data'); console.log(data);
return 'complete';}
run(generator).then((value) => { console.log(value); // complete});
// 出错啦// data// complete
复制代码

结语

本文讲述了异步函数的相关概念,并了解了异步函数其实只是语法糖,我们完全能够自己实现相同的功能,可以看到,生成器在这中间充当了非常重要的角色,以后可能还会基于生成器出现更多强大的模式。

参考资料

  • 《JavaScript 高级程序设计》

  • 《你不知道的 JavaScript》


发布于: 刚刚阅读数: 5
用户头像

yuanyxh

关注

站在巨人的肩膀上 2023-08-19 加入

web development

评论

发布
暂无评论
ES6 新特性详解 - 异步函数_js_yuanyxh_InfoQ写作社区