async,await與forEach引起的血案

本文僅是技術驗證,記錄,交流,不針對任何人。有冒犯的地方,請諒解。
該文首發於https://vsnail.cn/static/doc/blog/asyncForEach.htmlhtml

偶然間看到一篇文章,說起awaitforEach中不生效,asyncawait這個ES6中的語法,對於我來講應該也不陌生了,早在一兩年前就用過了,知道這是幹什麼用的,怎麼用的。在看完這篇文章後,「第七感」覺着自己這個標題彷佛有所不妥,內容到是感受沒啥問題,可是看到總結和餘下的評論,總覺的這裏面應該是有誤區了。所以想要扒一下「它的外套」,看看是啥牌子(嘿嘿嘿嘿。。。真的只是看牌子)。在看了幾篇詳細介紹asyncawait後,才發現寫決定寫這篇文章,是個錯誤。由於它太深了,牽扯太多了,感受就像無極中的饅頭同樣,能牽出一堆故事;也像是一個有實力,有背景的女一號,到處都是戲。java

這世上的一切你均可以獲得,只要你夠壞,而你,你還不夠壞 --《無極》es6

好了,話很少說,進入正題。讓咱們一塊兒來一件件的扒,看看究竟是什麼?編程

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可以達到咱們的目的,那麼用你的小指頭想一想就會有一個直接的結論。promise

原來async函數內部使用await後,能夠將await後面跟的異步函數變爲同步。數據結構

姑且認爲這個結論是正確的,那麼async函數又是如何實現的,纔能有如此神奇的效果?async函數返回的是函數裏面的return的值嗎?await只能跟異步函數嗎?異步

好的,帶着這些疑問,咱們繼續向下扒,看看究竟是A,仍是B仍是C、D、E、F、G。。。async

阮大神在《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徹底能夠勝任女一號的。

參考文獻

一、《重學 JS:爲啥 await 在 forEach 中不生效》juejin.im/post/5cb1d5…

二、《ECMAScript 6 入門》 es6.ruanyifeng.com/#docs/async

相關文章
相關標籤/搜索