本文僅是技術驗證,記錄,交流,不針對任何人。有冒犯的地方,請諒解。
該文首發於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