本文仅是技术验证,记录,交流,不针对任何人。有冒犯的地方,请谅解。
该文首发于https://vsnail.cn/static/doc/blog/asyncForEach.htmlhtml
偶然间看到一篇文章,说起await
在forEach
中不生效,async
,await
这个ES6
中的语法,对于我来讲应该也不陌生了,早在一两年前就用过了,知道这是干什么用的,怎么用的。在看完这篇文章后,“第七感”觉着自己这个标题彷佛有所不妥,内容到是感受没啥问题,可是看到总结和余下的评论,总觉的这里面应该是有误区了。所以想要扒一下“它的外套”,看看是啥牌子(嘿嘿嘿嘿。。。真的只是看牌子)。在看了几篇详细介绍async
,await
后,才发现写决定写这篇文章,是个错误。由于它太深了,牵扯太多了,感受就像无极中的馒头同样,能牵出一堆故事;也像是一个有实力,有背景的女一号,到处都是戏。java
这世上的一切你均可以获得,只要你够坏,而你,你还不够坏 --《无极》es6
好了,话很少说,进入正题。让咱们一块儿来一件件的扒,看看究竟是什么?编程
async
和await
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
可以达到咱们的目的,那么用你的小指头想一想就会有一个直接的结论。promise
原来async函数内部使用await后,能够将await后面跟的异步函数变为同步。数据结构
姑且认为这个结论是正确的,那么async函数又是如何实现的,才能有如此神奇的效果?async
函数返回的是函数里面的return
的值吗?await
只能跟异步函数吗?异步
好的,带着这些疑问,咱们继续向下扒,看看究竟是A,仍是B仍是C、D、E、F、G。。。async
阮大神在《ECMAScript 6 入门》中的async
函数一篇,提到这么一句话 “async 函数是什么?一句话,它就是 Generator 函数的语法糖。”异步编程
什么?async
是Generator
的语法糖?OK,那咱们再去扒下今天的女二号,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 toilet
,wash hands
,sit down
以及return
的eat
)。childEatProcess
和其余函数同样,直接调用便可。可是他的返回(必定要注意这里)不是return
的值,而是一个对象,一个指向内部状态的指针对象,也就是Iterator对象。
而且Generator
函数相似咱们家小朋友吃饭前的准备工做同样,你不去触发他,他是不会本身执行的。当ch
调用next
方法后,函数内部才开始执行,执行到yield
关键字后,运行yield
后面的表达式,而后就停下来了。等着你再去触发他(next
方法的调用)
因为 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*
表达式,用于将后面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";
}
复制代码
yield
表达式自己没有返回值,或者说老是返回undefined。next方法能够带一个参数,该参数就会被看成上一个yield表达式的返回值。。注意,这句话很是重要,是理解后面的根本。重要的事情说三遍,yield
表达式自己没有返回值,yield
表达式自己没有返回值,yield
表达式自己没有返回值。
做为本文的女三号,Iterator
,咱们就简单的扒一下吧。毕竟她不是这篇文章的小主。可是千万别小看她,这位也绝对是位重量级的女主,对象遍历,数组遍历,伪数组遍历,解构赋值,扩展符运算,全部能遍历的一切都离不开她的石榴裙。只是今天戏份略少而已。
Iterator
就是遍历器,它是一种接口,为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)。
Iterator
的遍历过程是这样的。
(1)建立一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,能够将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来讲,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
看了女三号(Iterator
)的我的简历。应该清楚Generator
函数执行后返回的对象就是一个内部指针的遍历器对象即Iterator
对象了吧。Iterator
对象再调用next
方法,遍历Generator
中全部yield
定义的状态。
以前描述女一号说,async
是generator
的语法糖,可是仍是没有看出来generator
和async
的关系呀。不急,咱们慢慢来。反过来先假如async是generator的语法糖这句话是正确的,那么咱们确定能够用generator
函数来写出async
的效果。
将async
拆解后,能够发现其实就两点:
async
中变为了同步,即await
后的异步表达式执行完后,才继续向下执行。generator
来讲,async
是自动执行的,而generator
返回的是iterator
,必需要调用next
,才能执行。OK,那咱们就按照这两点一个个的实现:
第一点,其实很简单,那么就是用回调函数,promise
等等均可以实现顺序执行。
有麻烦的是,要让Generator
函数自动运行,而不是咱们手动调用next
。
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
的原理其实就是Generator
函数和自执行器包装在一个函数里。因此才有async
是Generator
的语法糖的说法。真相大白,原来女一号就是穿了个马甲的女二号,只不过这个马甲赋予了女一号一些特别的能力。就像超人要穿他的战服才叫超人,才有超能力。
穿了马甲天然有些地方不同啦,虽然内部数据都同样。那么咱们来看看穿上马甲后,有什么不一样了。
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
异步表达式的返回值做为参数。也就是说若是async
在forEach
中有做用,那么后一个异步表达式确定会用前一个异步表达式的返回值作参数。也就是说咱们指望的输出效果应该是:
循环第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
的顺序执行。那么也就能说明async
在forEach
中是有做用的,只是场景不对罢了。
其实不管async
仍是generator
都还有不少点没有扒到。async
和generator
的出现对于异步函数的处理真的是一个质的飞跃,较于原来的回调函数的金字塔,promise
的非语义化来讲,async
彻底能够胜任女一号的。
一、《重学 JS:为啥 await 在 forEach 中不生效》juejin.im/post/5cb1d5…
二、《ECMAScript 6 入门》 es6.ruanyifeng.com/#docs/async