Async 函数的使用及简单实现

1)Async 函数概览

1.1 概念

通过在普通函数前加async操作符可以定义 Async 函数:

// 这是一个 async 函数
async function() {}

Async 函数体中的代码是异步执行的,不会阻塞后面代码执行,但它们的写法和同步代码相似。

Async 函数会 返回一个已完成的 promise 对象,实际在使用的时候会和await操作符配合使用,在介绍await之前,我们先看看 async 函数本身有哪些特点。

1.2 Async 函数基本用法

1.2.1 函数体内没有 await

如果 async 函数体内如果没有await操作符,那么它返回的 promise 对象状态和他的函数体内代码怎么写有关系,具体和 promise 的then()方法的处理方式相同:

1)没有显式 return 任何数据

此时默认返回Promise.resolve():

var a = (async () => {})();

相当于

var a = (async () => {
  return Promise.resolve();
})();

此时 a 的值:

a {
  [[PromiseStatus]]: 'resolved',
  [[PromiseValue]]: undefined
}

2)显式 return 非 promise

相当于返回Promise.resolve(data)

var a = (async () => {
  return 111;
})();

相当于

var a = (async () => {
  return Promise.resolve(111);
})();

此时 a 的值:

a {
  [[PromiseStatus]]: 'resolved',
  [[PromiseValue]]: 111
}

3)显式 return promise 对象

此时 async 函数返回的 promise 对象状态由显示返回的 promise 对象状态决定,这里以被拒绝的 promise 为例:

var a = (async () => Promise.reject(111))();

此时 a 的值:

a {
  [[PromiseStatus]]: 'rejected',
  [[PromiseValue]]: 111
}

但实际使用中,我们不会向上面那样使用,而是配合await操作符一起使用,不然像上面那样,和 promise 相比,并没有优势可言。特别的,没有await操作符,我们并不能用 async 函数解决相互依赖的异步数据的请求问题。

换句话说:我们不关心 async 返回的 promise 状态(通常情况,async 函数不会返回任何内容,即默认返回Promise.resolve()),我们关心的是 async 函数体内的代码怎么写,因为里面的代码可以异步执行且不阻塞 async 函数后面代码的执行,这就为写异步代码创造了条件,并且书写形式上和同步代码一样。

1.2.2 await 介绍

await操作符使用方式如下:

[rv] = await expression;

expression:可以是任何值,但通常是一个 promise;

rv: 可选。如果有且 expression 是非 promise 的值,则 rv 等于 expression 本身;不然,rv 等于 兑现 的 promise 的值,如果该 promise 被拒绝,则抛个异常(所以await一般被 try-catch 包裹,异常可以被捕获到)。

但注意await必须在 async 函数中使用,不然会报语法错误

1.2.3 await 使用

看下面代码例子:

1)expression 后为非 promise

(async () => {
  const b = await 111;
  console.log(b); // 111
})();

直接返回这个 expression 的值,即,打印 111

2)expression 为兑现的 promise

(async () => {
  const b = await Promise.resolve(111);
  console.log(b); // 111
})();

返回兑现的 promise 的值,所以打印111

3)expression 为拒绝的 promise

(async () => {
  try {
    const b = await Promise.reject(111);

    // 前面的 await 出错后,当前代码块后面的代码就不执行了
    console.log(b); // 不执行
  } catch (e) {
    console.log("出错了:", e); // 出错了:111
  }
})();

如果await后面的 promise 被拒绝或本身代码执行出错都会抛出一个异常,然后被 catch 到,并且,和当前await同属一个代码块的后面的代码不再执行。

2)Async 函数处理异步请求

2.1 相互依赖的异步数据

在 promise 中我们处理相互依赖的异步数据使用链式调用的方式,虽然相比回调函数已经优化很多,但书写及理解上还是没有同步代码直观。我们看下 async 函数如何解决这个问题。

先回顾下需求及 promise 的解决方案:

需求:请求 URL1 得到 data1;请求 URL2 得到 data2,但 URL2 = data1[0].url2;请求 URL3 得到 data3,但 URL3 = data2[0].url3

使用 promise 链式调用可以这样写代码:

promiseAjax 在 第二部分介绍 promise 时在 3.1 中定义的,通过 promise 封装的 ajax GET 请求。

promiseAjax('URL1')
  .then(data1 => promiseAjax(data1[0].url2))
  .then(data2 => promiseAjax(data2[0].url3);)
  .then(console.log(data3))
  .catch(e => console.log(e));

如果使用 Async 函数则可以像同步代码的一样写:

async function() {
  try {
    const data1 = await promiseAjax('URL1');
    const data2 = await promiseAjax(data1[0].url);
    const data3 = await promiseAjax(data2[0].url);
  } catch (e) {
    console.log(e);
  }
}

之所以可以这样用,是因为只有当前await等待的 promise 兑现后,它后面的代码才会执行(或者抛出错误,后面代码都不执行,直接去到 catch 分支)。

这里有两点值得关注:

1)await帮我们处理了 promise,要么返回兑现的值,要么抛出异常;
2)await在等待 promise 兑现的同时,整个 async 函数会挂起,promise 兑现后再重新执行接下来的代码。

对于第 2 点,是不是想到了生成器?在 1.4 节中我们会通过生成器 + promise 自己写一个 async 函数。

2.2 无依赖关系的异步数据

Async 函数没有Promise.all()之类的方法,我们需要写多几个 async 函数。

可以借助Promise.all()在同一个 async 函数中并行处理多个无依赖关系的异步数据,如下:

async function fn1() {
  try {
    const arr = await Promise.all([
      promiseAjax("URL1"),
      promiseAjax("URL2"),
    ]);

    // ... do something
  } catch (e) {
    console.log(e);
  }
}

感谢 @贾顺名评论

但实际开发中如果异步请求的数据是业务不相关的,不推荐这样写,原因如下:

把所有的异步请求放在一个 async 函数中相当于手动加强了业务代码的耦合,会导致下面两个问题:

1)写代码及获取数据都不直观,尤其请求多起来的时候;
2)Promise.all里面写多个无依赖的异步请求,如果 其中一个被拒绝或发生异常,所有请求的结果我们都获取不到

如果业务场景是不关心上面两点,可以考虑使用上面的写法,不然,每个异步请求都放在不同的 async 函数中发出。

下面是分开写的例子:

async function fn1() {
  try {
    const data1 = await promiseAjax("URL1");

    // ... do something
  } catch (e) {
    console.log(e);
  }
}

async function fn2() {
  try {
    const data2 = await promiseAjax("URL2");

    // ... do something
  } catch (e) {
    console.log(e);
  }
}

3)Async 模拟实现

3.1 async 函数处理异步数据的原理

我们先看下 async 处理异步的原理:

  • async 函数遇到await操作符会挂起;
  • await后面的表达式求值(通常是个耗时的异步操作)前 async 函数一直处于挂起状态,避免阻塞 async 函数后面的代码;
  • await后面的表达式求值求值后(异步操作完成),await可以对该值做处理:如果是非 promise,直接返回该值;如果是 promsie,则提取 promise 的值并返回。同时告诉 async 函数接着执行下面的代码;
  • 哪里出现异常,结束 async 函数。

await后面的那个异步操作,往往是返回 promise 对象(比如 axios),然后交给 await 处理,毕竟,async-await 的设计初衷就是为了解决异步请求数据时的回调地狱问题,而使用 promise 是关键一步。

async 函数本身的行为,和生成器类似;而await等待的通常是 promise 对象,也正因如此,常说 async 函数是 生成器 + promise 结合后的语法糖。

既然我们知道了 async 函数处理异步数据的原理,接下来我们就简单模拟下 async 函数的实现过程。

3.2 async 函数简单实现

这里只模拟 async 函数配合await处理网络请求的场景,并且请求最终返回 promise 对象,async 函数本身返回值(已完成的 promise 对象)及更多使用场景这里没做考虑。

所以接下来的 myAsync 函数只是为了说明 async-await 原理,不要将其用在生产环境中。

3.2.1 代码实现

/**
 * 模拟 async 函数的实现,该段代码取自 Secrets of the JavaScript Ninja (Second Edition),p159
 */
// 接收生成器作为参数,建议先移到后面,看下生成器中的代码
var myAsync = generator => {
  // 注意 iterator.next() 返回对象的 value 是 promiseAjax(),一个 promise
  const iterator = generator();

  // handle 函数控制 async 函数的 挂起-执行
  const handle = iteratorResult => {
    if (iteratorResult.done) return;

    const iteratorValue = iteratorResult.value;

    // 只考虑异步请求返回值是 promise 的情况
    if (iteratorValue instanceof Promise) {
      // 递归调用 handle,promise 兑现后再调用 iterator.next() 使生成器继续执行
      // ps.原书then最后少了半个括号 ')'
      iteratorValue
        .then(result => handle(iterator.next(result)))
        .catch(e => iterator.throw(e));
    }
  };

  try {
    handle(iterator.next());
  } catch (e) {
    console.log(e);
  }
};

3.2.2 使用

myAsync接收的一个生成器作为入参,生成器函数内部的代码,和写原生 async 函数类似,只是用yield代替了await

myAsync(function*() {
  try {
    const a = yield Promise.resolve(1);
    const b = yield Promise.resolve(a + 10);
    const c = yield Promise.resolve(b + 100);
    console.log(a, b, c); // 输出 1,11,111
  } catch (e) {
    console.log("出错了:", e);
  }
});

上面会打印1 11 111

如果第二个yield语句后的 promise 被拒绝Promise.reject(a + 10),则打印出错了:11

3.2.3 说明:

  • myAsync 函数接受一个生成器作为参数,控制生成器的 挂起 可达到使整个 myAsync 函数在异步代码请求过程 挂起 的效果;
  • myAsync 函数内部通过定义handle函数,控制生成器的 挂起-执行

具体过程如下:

1)首先调用generator()生成它的控制器,即迭代器iterator,此时,生成器处于挂起状态;
2)第一次调用handle函数,并传入iterator.next(),这样就完成生成器的第一次调用的;
3)执行生成器,遇到yield生成器再次挂起,同时把yield后表达式的结果(未完成的 promise)传给 handle;
4)生成器挂起的同时,异步请求还在进行,异步请求完成(promise 兑现)后,会调用handle函数中的iteratorValue.then()
5)iteratorValue.then()执行时内部递归调用handle,同时把异步请求回的数据传给生成器(iterator.next(result)),生成器更新数据再次执行。如果出错直接结束;
6)3、4、5 步重复执行,直到生成器结束,即iteratorResult.done === true,myAsync 结束调用。

引用文章:https://segmentfault.com/a/1190000015735201

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容

  • 你不知道JS:异步 第四章:生成器(Generators) 在第二章,我们明确了采用回调表示异步流的两个关键缺点:...
    purple_force阅读 960评论 0 2
  • 含义 async函数是Generator函数的语法糖,它使得异步操作变得更加方便。 写成async函数,就是下面这...
    oWSQo阅读 1,994评论 0 2
  • async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便。 async 函数是...
    huilegezai阅读 1,259评论 0 6
  • 一、async/await的优点 1)方便级联调用:即调用依次发生的场景; 2)同步代码编写方式: Promise...
    puxiaotaoc阅读 105,616评论 7 62
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,712评论 0 5