博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
async,await与forEach引发的血案
阅读量:6324 次
发布时间:2019-06-22

本文共 8834 字,大约阅读时间需要 29 分钟。

本文仅是技术验证,记录,交流,不针对任何人。有冒犯的地方,请谅解。

该文首发于https://vsnail.cn/static/doc/blog/asyncForEach.html

偶然间看到一篇文章,提及awaitforEach中不生效,asyncawait这个ES6中的语法,对于我来说应该也不陌生了,早在一两年前就用过了,知道这是干什么用的,怎么用的。在看完这篇文章后,“第七感”觉着本身这个标题似乎有所不妥,内容到是感觉没啥问题,但是看到总结和余下的评论,总觉的这里面应该是有误区了。因此想要扒一下“它的外套”,看看是啥牌子(嘿嘿嘿嘿。。。真的只是看牌子)。在看了几篇详细介绍asyncawait后,才发现写决定写这篇文章,是个错误。因为它太深了,牵扯太多了,感觉就像无极中的馒头一样,能牵出一堆故事;也像是一个有实力,有背景的女一号,处处都是戏。

这世上的一切你都可以得到,只要你够坏,而你,你还不够坏 --《无极》

好了,话不多说,进入正题。让我们一起来一件件的扒,看看到底是什么?

asyncawait

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。OK,看看如何操作的。

async function getBookInfo(name){    const baseInfo = await requestBookBaseInfo(name); //requestBookBaseInfo 方法发送一个请求,向后台请求数据。这是一个异步方法    const bookPrice = await requestBookPrice(baseInfo.id); //requestBookPrice方法发送一个请求,向后台请求数据。这是一个异步方法    return {..baseInfo,bookPrice};}复制代码

getBookInfo方法中,有两个异步函数,并且第二个异步函数用到了第一个异步函数的结果。如果getBookInfo能够达到我们的目的,那么用你的小指头想想就会有一个直接的结论。

原来async函数内部使用await后,可以将await后面跟的异步函数变为同步。

姑且认为这个结论是正确的,那么async函数又是如何实现的,才能有如此神奇的效果?async函数返回的是函数里面的return的值吗?await只能跟异步函数吗?

好的,带着这些疑问,我们继续向下扒,看看到底是A,还是B还是C、D、E、F、G。。。

阮大神在《ECMAScript 6 入门》中的async函数一篇,提到这么一句话 “async 函数是什么?一句话,它就是 Generator 函数的语法糖。

什么?asyncGenerator的语法糖?OK,那我们再去扒下今天的女二号,Generator

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。---这也是阮大神说的。

按我个人的理解,Generator英语直译“生成”,那么Generator函数其实就是一个生成器,生成什么呢,生成的就是一个Iterator。等等又出现个Iterator,这又是什么?好吧,我们姑且将她放置一边,毕竟都女三号了,没这么快入场。如果不了解女三号,那么我们也可以将Generator理解为状态管理机。毕竟伟大诗人曾说过“横看成岭侧成峰”,我们现在也只是转个角度欣赏女二号而已。

在形式上,Generator只是一个普通的函数而已,只不过有两个比较明显的特征。一个是在关键字function和函数名之间有个*;二,在函数内部使用yield表达式,定义不同的状态(注意这里,这就是为什么又称之为状态管理机的由来)。

function* childEatProcess() {  yield 'use toilet';  yield 'wash hands';  yield 'sit down';  return 'eat'}var ch = childEatProcess();ch.next();//{value:'use toilet',done:false}ch.next();//{value:'wash hands',done:false}ch.next();//{value:'sit down',done:false}ch.next();//{value:'eat',done:true}ch.next();//{value:'undefined',done:true}复制代码

上面的代码定义了一个Generator函数,他的内部有三个yield,也就是说该函数有四个状态(use toiletwash handssit down以及returneat)。childEatProcess和其他函数一样,直接调用即可。但是他的返回(一定要注意这里)不是return的值,而是一个对象,一个指向内部状态的指针对象,也就是Iterator对象

并且Generator函数类似我们家小朋友吃饭前的准备工作一样,你不去触发他,他是不会自己执行的。当ch调用next方法后,函数内部才开始执行,执行到yield关键字后,运行yield后面的表达式,然后就停下来了。等着你再去触发他(next方法的调用)

yield表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield* 表达式

和普通的yield表达式相比,yield*表达式多了一个星号。yield*表达式,用于将后面Generator表达式执行。这个还真不好表达,来看看下面的代码,直观感受下。

function* generator_1(){    yield "b";    yield "c";}function* generator_2(){    yield "a";    yield generator_1();    yield "d";}function* generator_3(){    yield "a";    yield* generator_1();    yield "d";}let g2 = generator_2();g2.next();//{value:"a",done:false}g2.next();//{value:Iterator,done:false}g2.next();//{value:"d",done:true}g2.next();//{value:undefined,done:true}let g3 = generator_3();g3.next();//{value:"a",done:false}g3.next();//{value:"b",done:false}g3.next();//{value:"c",done:false}g3.next();//{value:"d",done:false}复制代码

从上面的列子,可以看出yield只是执行了generator函数而已,也就是获取到generator函数生成的iterator而已。而yield*,确是执行了generator函数的内部指针。

那么也可以将代码

function* generator_1(){    yield "b";    yield "c";}function* generator_3(){    yield "a";    yield* generator_1();    yield "d";}//上面的代码等价于function* generator_4(){    yield "a";    yield "b";    yield "c";    yield "d";}复制代码

next的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。。注意,这句话非常重要,是理解后面的根本。重要的事情说三遍,yield表达式本身没有返回值yield表达式本身没有返回值yield表达式本身没有返回值

Iterator

作为本文的女三号,Iterator,我们就简单的扒一下吧。毕竟她不是这篇文章的小主。但是千万别小看她,这位也绝对是位重量级的女主,对象遍历,数组遍历,伪数组遍历,解构赋值,扩展符运算,所有能遍历的一切都离不开她的石榴裙。只是今天戏份略少而已。

Iterator就是遍历器,它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

再扒Generator

看了女三号(Iterator)的个人简历。应该清楚Generator函数执行后返回的对象就是一个内部指针的遍历器对象即Iterator对象了吧。Iterator对象再调用next方法,遍历Generator中所有yield定义的状态。

之前描述女一号说,asyncgenerator的语法糖,但是还是没有看出来generatorasync的关系呀。不急,我们慢慢来。反过来先假如async是generator的语法糖这句话是正确的,那么我们肯定可以用generator函数来写出async的效果。

async拆解后,可以发现其实就两点:

  1. 内部的异步函数在async中变为了同步,即await后的异步表达式执行完后,才继续向下执行。
  2. 对于generator来说,async是自动执行的,而generator返回的是iterator,必须要调用next,才能执行。

OK,那我们就按照这两点一个个的实现:

第一点,其实很简单,那么就是用回调函数,promise等等都可以实现顺序执行。

有麻烦的是,要让Generator函数自动运行,而不是我们手动调用next

自动执行Generator

Thunk 函数是自动执行 Generator 函数的一种方法。

很早很早以前,有一个争论的焦点就是"求值策略",即函数的参数到底应该何时求值。有人觉的应该在使用的时候表达式才求值,这样避免不必要的计算,相当于传名调用。有人认为应该在使用前就将表达式计算好,相当于传值调用

Thunk则是传名调用的实现,是将参数放到一个临时函数之中,再将这个临时函数传入函数体。

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。似乎和函数柯理化一个概念了。

function readSome(a,b,callBack){    setTimeout(function(){        callBack && callBack(a+b);    },200)}let thunkFun = function(fn){    return function(...args){        return function(callBack){           return  fn.call(this,...args,callBack);        }    }}let thunk_rs = thunkFun(readSome);thubk_rs('Hi','girl')(function(str){    console.log(str);})复制代码

你可能会问, Thunk 函数有什么用?和Generator自执行有什么关系。。慢慢来,衣服是一件件扒,一件件穿的。

function* gen() {  // ...}var g = gen();var res = g.next();while(!res.done){  console.log(res.value);  res = g.next();}复制代码

上面代码中,Generator 函数gen会自动执行完所有步骤。 但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。这时,Thunk 函数就能派上用处。

function readSome(a,b,callBack){    setTimeout(function(){        callBack && callBack(a+b);    },200)}let thunkFun = function(fn){    return function(...args){        return function(callBack){           return  fn.call(this,...args,callBack);        }    }}let thunk_rs = thunkFun(readSome);var gen = function* (){  var r1 = yield thunk_rs('Hi','girl');  console.log(r1.toString());  var r2 = yield readFileThunk('you are ','beautiful');  console.log(r2.toString());};function run(fn){    var gen = fn();    function next(err,data){        let rs = gen.next(data);        if(rs.done) return ;        rs.value(next)    }    next();}run(gen)复制代码

似乎这就完美的完成了自动执行。当然自动执行并不仅仅这一种方式。

async原理

通过之前的了解,我们知道async的原理其实就是Generator函数和自执行器包装在一个函数里。所以才有asyncGenerator的语法糖的说法。真相大白,原来女一号就是穿了个马甲的女二号,只不过这个马甲赋予了女一号一些特别的能力。就像超人要穿他的战服才叫超人,才有超能力。

再扒async

穿了马甲自然有些地方不一样啦,虽然内部数据都一样。那么我们来看看穿上马甲后,有什么不同了。

async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。就是我们所谓的自执行。

(2)更好的语义。

(3)更广的适用性。

(4)返回值promise

最重要的是第一和第四点。第一点,地球人都知道,不说了。第四点,返回promise对象,而generator返回的iterator对象。这是非常重要的差异点。

实际上async函数执行的时候,一旦遇到await就会先返回(返回一个promise对象),等到异步操作完成,再接着执行函数体内后面的语句。async函数内部return语句返回的值,会成为then方法回调函数的参数。

解析那个馒头

为了一个馒头引发了一场血案,为了一篇文章引发了今天的扒衣行动。那我们回过头来再来看看这篇文章《为啥await在forEach中不生效》。

文章中有这么一段代码:

function test() {  let arr = [3, 2, 1]  arr.forEach(async item => {    const res = await fetch(item)    console.log(res)  })  console.log('end')}function fetch(x) {  return new Promise((resolve, reject) => {    setTimeout(() => {      resolve(x)    }, 500 * x)  })}test()复制代码

看了文章,大概了解这段代码其实是想做一个事情,虽然异步了,但是想按照数组排序顺序显示数组中的元素。使用forEach遍历,没有实现这个需求。所以才有了文章的标题。但是女一号表示这个锅,我不背。不是我不行,而是你没把我安排到好的剧本中。

来,我们换个剧本,依然在forEach里面,但是呢,在里面的回调函数中做点文章。

function test() {    let arr = ["a", "b", "c"]    arr.forEach(async (item,index) => {      console.log('循环第'+index+'次')      const res = await fetch(item)      console.log('res',res)      const res1 = await fetch1(res);      console.log('res1',res1)    })    console.log('end')  }    function fetch(x,index) {    return new Promise((resolve, reject) => {      setTimeout(() => {        resolve(x+"经过fetch处理")      }, 500)    })  }    function fetch1(x) {    return new Promise((resolve, reject) => {      setTimeout(() => {        resolve(x+" 经过fetch1处理")      }, 100)    })  }    test()复制代码

这个剧本,async函数里面对两个异步表达式设置了await。并且都是后一个await的异步表达式使用了前一个await异步表达式的返回值作为参数。也就是说如果asyncforEach中有作用,那么后一个异步表达式肯定会用前一个异步表达式的返回值做参数。也就是说我们期望的输出效果应该是:

循环第0次

循环第1次
循环第2次
end
undefined
res a经过fetch处理
res b经过fetch处理
res c经过fetch处理
res1 a经过fetch处理 经过fetch1处理
res1 b经过fetch处理 经过fetch1处理
res1 c经过fetch处理 经过fetch1处理

亲们,你们可以试试,是不是这样子的输出。嘿嘿,我已经试了,确实是这样子输出的。

我们来看看为什么剧本一达不到预期的目的,而剧本二达到了预期的目的?很简单,async函数返回的是什么,返回的是promise,是一个异步对象。而forEach是一个个的回调函数,也就是说这些回调函数会立即执行,当执行到一个await关键字附近的时候,就会返回一个promise对象,async函数内部被冻结,等待await后面的异步表达式执行完后,再执行async函数内部的剩余代码。因此剧本一forEach时得到的是一堆的promise对象,而不是async函数内部的执行结果。async函数保证的是函数内部的await的顺序执行。那么也就能说明asyncforEach中是有作用的,只是场景不对罢了。

总结

其实无论async还是generator都还有很多点没有扒到。asyncgenerator的出现对于异步函数的处理真的是一个质的飞跃,较于原来的回调函数的金字塔,promise的非语义化来说,async完全可以胜任女一号的。

参考文献

1、《重学 JS:为啥 await 在 forEach 中不生效》

2、《ECMAScript 6 入门》

转载地址:http://mpqaa.baihongyu.com/

你可能感兴趣的文章
Chrome各个版本小常识
查看>>
阿里云图片压缩上传代码
查看>>
java关于split分割字符串,空的字符串不能得到的问题
查看>>
JavaScript函数式编程
查看>>
C++_系列自学课程_第_6_课_bitset集_《C++ Primer 第四版》
查看>>
java对象数组
查看>>
Android中使用dimen定义尺寸(转)
查看>>
Webserver管理系列:11、注意默认的隐含共享
查看>>
《学习OpenCv》 笔记(1)
查看>>
不同平台下Java环境变量的设置
查看>>
14、Java中用浮点型数据Float和Double进行精确计算时的精度问题
查看>>
[百度]数组A中任意两个相邻元素大小相差1,在其中查找某个数
查看>>
温故而知新:Delegate,Action,Func,匿名方法,匿名委托,事件
查看>>
2015 UESTC 数据结构专题G题 秋实大哥去打工 单调栈
查看>>
mysql触发器的作用及语法
查看>>
strtok、strtok_s、strtok_r 字符串切割函数
查看>>
shell编程基础(5)---循环指令
查看>>
八皇后问题
查看>>
稀疏矩阵
查看>>
Android源码
查看>>