「硬核JS」深刻了解異步解決方案

前言

Javascript語言的執行環境是單線程(single thread,指一次只能完成一件任務,若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推)javascript

這種模式的好處是實現起來比較簡單,執行環境相對單純,壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行,常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行html

爲了解決這個問題,Javascript將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)前端

同步模式 就是後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的java

異步模式則徹底不一樣,每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的,在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,異步模式甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低jquery

前面都是些無用的話,由於你們都對此很清楚,那麼,問題來了,你瞭解幾種異步解決方案?git

本文會由淺入深的敘述下面幾種已知的異步解決方案,以及它們的區別程序員

  • 回調函數(callback)
  • 事件監聽(發佈/訂閱)解析
  • Promise解析及從0~1的源碼體驗
  • Generator全面解析
  • Async/Await解析

遇上春節不出門爲國家作貢獻,寫了這篇帖子,本文有點長,由於原本要寫四篇文章分別敘述,可是我以爲仍是在一塊看比較容易對比理解,大概有兩萬字左右,若是你肯花20分鐘的時間閱讀本文,定會有所收穫,我在耐心寫,也但願你們能夠耐心看完,基礎不太好的同窗能夠分塊看,已詳細註明各級標題,但願經過本文可讓你們對大JS異步編程加深瞭解es6

哦對了,先贊在看,養成習慣,畢竟碼字不易,你們的每個贊和評論都將爲我碼下一篇文章添一些動力,多謝😁github

回調函數(callback)

簡述回調函數

回調函數你們都應該清楚,簡單理解就是一個函數被做爲參數傳遞給另外一個函數面試

回調並不必定就是異步,並無直接關係,只不過回調函數是異步的一種解決方案

咱們用例子來簡單說明下

function fn1(callback){
  console.log("1")
  callback && callback()
}

function fn2(){
  console.log("2")
}

fn1(fn2)
複製代碼

如上代碼所示,函數fn1參數爲一個回調,調用fn1時傳進入了函數fn2,那麼在函數fn1執行到callback函數調用時會調用fn2執行,這是一個典型的回調函數,不過是同步的,咱們能夠利用這點來解決異步,以下

fn1(callback){
  setTimeout(() => {
    callback && callback()
  }, 1000)
}

fn1(()=>{
  console.log("1")
})
複製代碼

如上所示,咱們使用setTimeout在函數fn1中模擬了一個耗時1s的任務,耗時任務結束會拋出一個回調,那麼咱們在調用時就能夠作到在函數fn1的耗時任務結束後執行回調函數了

採用這種方式,咱們把同步操做變成了異步操做,fn1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行

回調函數優/缺

優勢

一句話,回調函數是異步編程最基本的方法,其優勢是簡單、容易理解和部署

缺點

回調函數最大的缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling)

fun1(() => {
  fun2(() => {
    fun3(() => {
      fun4(() => {
        fun5(() => {
          fun6()
        })
      })
    })
  })
})
複製代碼

上面這種代碼在以前使用AJAX請求時很常見,由於業務上在一個請求結束後發起另外一個請求的需求太多了,代碼不優雅,不易閱讀維護,高耦合,層層嵌套形成這種回調地獄

異步回調中,回調函數的執行棧與原函數分離開,外部沒法抓住異常,異常會變得不可控

雖然缺點多,但回調函很多天常開發中也不可或缺,使用時注意就行了

回調函數比較簡單經常使用,就先介紹到這裏,接下來咱們看事件監聽

事件監聽(發佈訂閱模式)

解決異步,能夠採用事件驅動,任務的執行不取決於代碼的順序,而取決於某個事件是否發生

在阮一峯老師早期發佈的 Javascript異步編程的4種方法(參考連接【1】) 一文中,把事件監聽和發佈訂閱做爲了避免同的兩種解決方案,可是我我的以爲這兩種徹底能夠併爲一種,都是利用了發佈訂閱模式的事件驅動,因此就放一塊解釋了

JQuery實現事件監聽

jquery實現比較簡單,由於jq爲咱們封裝好了方法,使用便可,只是JQ不經常使用了,簡單瞭解下

咱們可使用jquery中的on來監聽事件,使用trigger觸發事件,以下

$("body").on("done", fn2)

function fn1() {
  setTimeout(function() {
    $("body").trigger("done")
  }, 2000)
}

function fn2() {
  console.log("fn2執行了")
}
fn1()
複製代碼

咱們使用jq的on監聽了一個自定義事件done,傳入了fn2回調,表示事件觸發後當即執行函數fn2

在函數fn1中使用setTimeout模擬了耗時任務,setTimeout回調中使用trigger觸發了done事件

咱們可使用on來綁定多個事件,每一個事件能夠指定多個回調函數

JavaScript實現事件監聽

在JS中咱們要本身實現相似JQ的ontrigger

實現的過程當中用到了一個設計模式,也就是發佈訂閱模式,因此簡單提一下

簡述發佈訂閱模式(觀察者模式)

發佈訂閱模式(publish-subscribe pattern),又叫觀察者模式(observer pattern),定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知

來看一個比較挫的例子

小李辛辛苦苦作了兩年程序猿,攢了些錢,心裏激動,要去售樓部買一個心儀已久的房型

到售樓部問了下,售樓部說暫時沒有這種房型的房源了,怎麼辦呢,下次再來吧

可是小李不知道這種房型何時有房源,總不能天天打電話到售樓部問吧,小李就把電話和房型信息留到售樓部了,何時有這種房源了,售樓部會短信通知

要知道,售樓部不會只通知小李一我的,售樓部會把預留信息全部房型信息一致的人都通知一遍

在這個比較挫的例子中,小李包括每一個買房的人都是訂閱者,而售樓部就是發佈者
複製代碼

其實咱們都用過發佈訂閱模式,好比咱們在DOM節點上綁定一個事件函數,就已經使用了

document.body.addEventListener('click', function () {
  console.log(1)
})
複製代碼

可是這只是對發佈訂閱模式最簡單的使用,在不少場景下咱們常常會實現一些自定義事件來知足咱們的需求

好比咱們下面要防照JQ那種來寫一個自定義事件監聽器,須要監聽一個事件,在該事件觸發時執行其監聽回調

發佈訂閱模式實現事件監聽器

發佈訂閱模式有不少種實現方式,下面咱們用class來簡單實現下

class Emitter {
  constructor() {
    // _listener數組,key爲自定義事件名,value爲執行回調數組-由於可能有多個
    this._listener = []
  }

  // 訂閱 監聽事件
  on(type, fn) {
    // 判斷_listener數組中是否存在該事件命
    // 存在將回調push到事件名對應的value數組中,不存在直接新增
    this._listener[type] 
      ? this._listener[type].push(fn) 
    	: (this._listener[type] = [fn])
  }

  // 發佈 觸發事件
  trigger(type, ...rest) {
    // 判斷該觸發事件是否存在
    if (!this._listener[type]) return
    // 遍歷執行該事件回調數組並傳遞參數
    this._listener[type].forEach(callback => callback(...rest))
  }
}
複製代碼

如上所示,咱們建立了一個Emitter類,而且添加了兩個原型方法ontrigger,上面代碼中均有註釋,因此不過多解釋了,基礎很差的同窗多看幾遍本身敲一下,比較簡單

使用時

// 建立一個emitter實例
const emitter = new Emitter()

emitter.on("done", function(arg1, arg2) {
  console.log(arg1, arg2)
})

emitter.on("done", function(arg1, arg2) {
  console.log(arg2, arg1)
})

function fn1() {
  console.log('我是主程序')
  setTimeout(() => {
    emitter.trigger("done", "異步參數一", "異步參數二")
  }, 1000)
}

fn1()
複製代碼

如上所示,咱們先建立一個emitter實例,接着註冊事件,再觸發事件,用法和上面JQ雷同,均解決了異步問題

Vue的實現就是一個比較複雜的發佈訂閱模式,使用Vue的同窗,上面的這個事件監聽器,把trigger名字改爲emit是否是就眼熟多了,固然咱們這個比較簡單,畢竟代碼就那麼六七行,不過理是這麼個理

事件監聽優/缺

優勢

發佈訂閱模式實現的事件監聽,咱們能夠綁定多個事件,每一個事件也能夠指定多個回調函數,仍是比較符合模塊化思想的,咱們自寫監聽器時能夠作不少優化從而更好的監控程序運行

缺點

整個程序變成了事件驅動,流程上來講或多或少都會有點影響,每次使用還得註冊事件監聽再進行觸發挺麻煩的,代碼也不太優雅,並非事件驅動很差,畢竟需求只是 解決異步問題 而已,況且有更優解

Promise

Promise簡述

ES2015 (ES6)標準化和引入了Promise對象,它是異步編程的一種解決方案

簡單來講就是用同步的方式寫異步的代碼,可用來解決回調問題

Promise特色

特色一

Promise,承諾執行,Promise對象的狀態是不受外界影響的

Promise對象表明一個異步操做,它有三種狀態

  • 進行中 (Pending)

  • 已完成 (Resolved/Fulfilled)

  • 已失敗 (Rejected)

只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態

這就是Promise這個名字的由來,它的英語意思就是承諾,表示其餘手段沒法改變

特色二

Promise對象狀態一旦改變,就不會再變

Promise對象的狀態改變,只有兩種可能

  • 從Pending變爲Resolved

  • 從Pending變爲Rejected

只要這兩種狀況發生,狀態就凝固,不會再變了,會一直保持這個結果

Promise使用

Promise是一個構造函數,咱們能夠經過new關鍵字來建立一個Promise實例,也能夠直接使用Promise的一些靜態方法

new一個Promise實例

語法

new Promise( function(resolve, reject) {...});
複製代碼

示例

function fn1(){
  return new Promise((resolve,reject) => {
    setTimeout(()=>{
      let num = Math.ceil(Math.random()*10)
      if(num < 5){
        resolve(num)
      }else{
        reject('數字太大')
      }
    },2000)
  })
}
複製代碼

如上所示,咱們使用new關鍵字建立了一個promise實例,並在函數fn1中return了出來

new Promise建立了一個promise實例,Promise構造函數會把一個叫作處理器函數(executor function)的函數做爲它的參數

處理器函數接收兩個參數分別是resolvereject,這兩個參數也是兩個回調函數

resolve 函數在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去

reject 函數在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去

簡單理解就是一個是成功回調,一個是失敗回調

Promise.prototype.then()

Promise對象有一個原型方法then

Promise實例生成之後,能夠用then方法指定resolved狀態和reject狀態的回調函數

Promise.prototype.then(onFulfilled[, onRejected])
複製代碼

then方法接收兩個回調onFulfilled和onRejected

  • onFulfilled-可選

    • 當 Promise 變成已完成狀態(fulfilled)時調用的回調函數
    • 該函數有一個參數,即接受的最終結果(the fulfillment value)
    • 若是該參數不是函數,則會在內部被替換爲 (x) => x,即原樣返回promise最終結果的函數
  • onRejected-可選

    • 當 Promise 變成接受狀態或拒絕狀態(rejected)時調用的回調函數
    • 該函數有一個參數,即拒絕的緣由(rejection reason)
    • 若是該參數不是函數,則會在內部被替換爲一個 Thrower 函數(it throws an error it received as argument)

then方法在接收一個promise實例後會返回一個新的Promise實例(並非原來那個Promise實例),且原來的promise實例的返回值將做爲參數傳入這個新Promise的resolve函數

那麼既然then方法返回一個新的promise實例,因此咱們能夠接着使用then方法,即鏈式調用,也被稱爲 **複合(composition)**操做

接上面的示例,函數fn1會返回一個promise實例

fn1()
  .then((data)=>{
    console.log(data)
  },(err)=>{
    console.log(err)
  })
複製代碼

如上所示,咱們使用了then方法的兩個參數

第一個參數回調咱們很經常使用,其實就是 Promise 變成已完成狀態且拿到傳遞的值

第二個參數回調就是 Promise 變成接受狀態或拒絕狀態且拿到錯誤參數,咱們可能用的少,通常都是用catch方法,then方法的第二個參數onRejected和catch仍是有一些細微區別的,下面會提到

根據Promises/A+中對then方法的定義,咱們來看then方法的特色

首先then 方法必須返回一個 promise 對象(劃重點)

鏈式調用的原理,不管是何種狀況then方法都會返回一個新的Promise對象,這樣纔會有下個then方法

若是then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise對象返回

就像下面這個例子,then方法接收Promise對象,then方法中返回一個普通值時,下一個then中是能夠接到的

let p =new Promise((resolve,reject)=>{
  resolve(1)
})

p.then(data=>{
  return 2	// 返回了一個普通值
}).then(data=>{
  console.log(data) // 2
})
複製代碼

若是then方法中沒有return語句,就返回一個用Undefined包裝的Promise對象

以下面例子的輸出結果

let p = new Promise((resolve, reject) => {
  resolve(1)
})

p.then(data => {
  // 無return語句
}).then(data => {
  console.log(data) // undefined
})
複製代碼

若是then方法中出現異常,則調用失敗態方法(reject)跳轉到下一個then的onRejected

then方法的第二個參數onRejected是監測不到當前then方法回調異常的,規範中定義當前then方法出現異常則調用失敗態方法(reject)流轉到下一個then的onRejected

let p = new Promise((resolve, reject) => {
  resolve(1)
})

p.then(data => 2)
  .then(
    data => {
      throw "this is err"
    },
    err => {
      console.log("err1:" + err)
    }
  )
  .then(
    data => {
      console.log(data)
    },
    err => {
      console.log("err2:" + err) // err2:this is err
    }
  )
複製代碼

若是then方法沒有傳入任何回調,則繼續向下傳遞(即所謂的值穿透)

下面示例,在第一個then方法以後連續調用了兩個空的then方法 ,沒有傳入任何回調函數,也沒有返回值,此時Promise會將值一直向下傳遞,直到接收處理,這就是所謂的值穿透

let p = new Promise((resolve, reject) => {
  resolve(1)
})

p.then(data => 2)
.then()
.then()
.then(data => {
  console.log(data) // 2
})
複製代碼

若是then方法中返回了一個Promise對象,那就以這個對象爲準,返回它的結果

話很少說,來看示例

let p = new Promise((resolve, reject) => {
  resolve(1)
})

p.then(data => {
  return new Promise((resolve, reject) => {
    resolve(2)
  })
}).then(data => {
  console.log(data) // 2
})
複製代碼

Promise.prototype.catch()

除了原型方法then以外,Promise對象還有一個catch的原型方法

catch方法能夠用於promise組合中的錯誤處理,此方法返回一個Promise,而且處理拒絕的狀況

p.catch(onRejected);

p.catch(function(reason) {
  // 拒絕
});
複製代碼
  • onRejected
    • 當Promise 被rejected時,被調用的一個回調函數,該函數擁有一個參數爲失敗緣由或錯誤信息

簡單理解就是捕獲異常,promise組合中拋出了錯誤或promise組合中出現rejected會被捕獲

一樣接最上面的示例,還使用fn1函數

fn1()
  .then(data=>{
    console.log(data)
  }).catch(err=>{
    console.log(err)
  })
複製代碼

使用這種方式捕獲錯誤或失敗是否是比then方法的第二個參數看着舒服了點呢,畢竟Promise就是鏈式到底

一樣也須要注意一點,catch方法也返回一個新的promise實例,若是 onRejected回調拋出一個錯誤或返回一個自己失敗的 Promise ,經過 catch 返回的Promise 會被rejected,不然,它就是一個成功的(resolved)promise實例

和上面的then方法中的第二個參數幾乎是一致的,咱們看例子

fn1()
  .catch(err => {
    console.log(err)
    return err
  })
  .then(data => {
    console.log(data)
  })
	.catch(err => {
    console.log(err)
  })
複製代碼

上面的fn1函數有一半的概率返回一個rejected,當返回一個rejected時下面的then方法回調中一樣會輸出,由於咱們在第一個catch中只return了錯誤信息,並無拋出錯誤或者返回一個失敗promise,因此第一個catch執行返回的promise對象是resolveing

Promise.prototype.finally()

finally,英文是最後的意思,此方法是ES2018的標準

原型方法finally,咱們使用的可能很少,語法以下

p.finally(onFinally);

p.finally(function() {
  // 返回狀態爲(resolved 或 rejected)
});
複製代碼

一句話便可解釋finally,在promise結束時,無論成功仍是失敗都將執行其onFinally回調,該回調無參數

適用於一樣的語句須要在then()catch()中各寫一次的狀況

Promise.resolve()

一句話歸納Promise.resolve()方法,接收一個值,將現有對象轉爲Promise 對象

Promise.resolve(value)
複製代碼

以下所示,該值可爲任意類型,也但是一個Promise對象

const p = Promise.resolve(123);

Promise.resolve(p).then((value)=>{
  console.log(value); // 123
});
複製代碼

Promise.reject()

Promise.reject()方法同上面Promise.resolve()同樣,只不過是返回一個帶有拒絕緣由的Promise對象

Promise.reject(123)
  .then(data => {
    console.log(data)
  })
  .catch(err => {
    console.log("err:" + err)
  })

// err:123
複製代碼

Promise.all()

Promise.all(iterable)用於將多個Promise 實例包裝成一個新的 Promise實例,參數爲一組 Promise 實例組成的數組

iterable類型爲ES6標準引入,表明可迭代對象,ArrayMapSet都屬於iterable類型 ,iterable下面咱們會講到,這裏咱們就先把這個參數理解成數組就能夠,稍後配合下面的iterable來理解

let p1 = Promise.resolve(1);
let p2 = Promise.resolve(2);
let p3 = Promise.resolve(3);

let p = Promise.all([p1,p2,p3]);

p.then(data=>{
  console.log(data) // [1,2,3]
})
複製代碼

如上所示,當 p1, p2, p3 狀態都 Resolved 的時候,p 的狀態纔會 Resolved

只要有一個實例 Rejected ,此時第一個被 Rejected 的實例返回值就會傳遞給 P 的回調函數

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = Promise.reject(3)

let p = Promise.all([p1, p2, p3])
p.then(data => {
  console.log(data)
}).catch(err => {
  console.log("err:" + err) // 3
})
複製代碼

應用場景在咱們有一個接口,須要其餘兩個或多個接口返回的數據做爲參數時會多一些

Promise.race()

Promise.race(iterable)和上面Promise.all(iterable)相似

all方法是迭代對象中狀態所有改變纔會執行

race方法正好相反,只要迭代對象中有一個狀態改變了,它的狀態就跟着改變,並將那個改變狀態實例的返回值傳遞給回調函數

const p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "1")
})

const p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "2")
})

Promise.race([p1, p2])
  .then(value => {
    console.log(value) // 2
  })
複製代碼

Promise.try()

開發中,常常遇到一種狀況:不知道或不想區分,函數fn是同步函數仍是異步函數,但想用 Promise 來處理它

由於這樣就能夠無論fn是否是異步操做,都用then方法指定下一步流程,用catch方法處理fn拋出的錯誤

咱們可能會使用Promise.resolve把它轉換成Promise對象

let fn = () => console.log("fn")
Promise.resolve(fn).then(cb => cb())
console.log("hahaha")

// hahaha
// fn
複製代碼

可是這樣有一個問題,若是函數fn是同步的,那麼這波操做會把它轉成異步,如上輸出

那麼有沒有一種方法,讓同步函數同步執行,異步函數異步執行,而且讓它們具備統一的 API 呢?固然能夠

咱們能夠這樣

const fn = () => console.log('fn');
(
  () => new Promise(
    resolve => resolve(fn())
  )
)().then(()=>{
  console.log(222)
})
.catch(err=>{
  console.log(err)
})

console.log('111');

// fn
// 111
// 222
複製代碼

也能夠這樣

const fn = () => console.log("fn");
(async () => fn())()
  .then(() => {
    console.log(222)
  })
  .catch(err => {
    console.log(err)
  })

console.log("111")

// fn
// 111
// 222
複製代碼

可是,代碼有點詭異,不優雅

來看使用try方法

const fn = () => console.log("fn");
Promise.try(fn)
  .then(() => {
    console.log(222)
  })
  .catch(err => {
    console.log(err)
  })

console.log("111")

// fn
// 111
// 222
複製代碼

如上所示,簡潔明瞭,仍是很實用的

其實,Promise.try就是模擬try代碼塊,就像promise.catch模擬的是catch代碼塊

最後 Promise.try 並非 Javascript 的一部分

早在16年有過這個提案,有興趣的同窗能夠了解下,如今也沒下文了,並無被歸入標準

若是想要使用的話,須要使用 Promise 庫Bluebird、Q等,或引入Polyfill

雖然沒有被歸入標準,但並不表明它很差用,你們自行體驗

想要了解更多此方法推薦你們看參考連接【4】【5】

onRejected和catch區別

上面提到了promise.then(onFulfilled, onRejected)中的第二個參數onRejected和catch

看到這你們可能會問,一樣都是捕獲異常它們的區別在哪

其實promise.then(onFulfilled, onRejected)onFulfilled回調中發生異常的話,在onRejected中是捕獲不到這個異常的,使用catch能夠捕獲到前面的onFulfilled的異常

其實這不算個缺點,咱們徹底能夠在末尾多加一個then從而達到和catch相同的做用,以下

Promise.reject(1)
  .then(() => {
    console.log("我是對的")
  })
  .then(null, err => {
    console.log("err:" + err) // err:1
  })

// 等價於

Promise.reject(1)
  .then(() => {
    console.log("我是對的")
  })
  .catch(err => {
    console.log("err:" + err) // err:1
  })
複製代碼

就這麼點區別,不過大部分人都喜歡直接使用catch罷了

then中拋錯未處理

若是在then中拋錯,而沒有對錯誤進行處理(catch),那麼會一直保持reject狀態,直到catch了錯誤

咱們來看一段代碼

Promise.resolve()
	.then(()=>{
  	console.log(a)
  	console.log("Task 1");
	})
  .then(()=>{
  	console.log("Task 2");
	})
  .catch((err)=>{
  	console.log("err:" + err)
	})
  .then(()=>{
  	console.log("finaltask")
	});

// err:ReferenceError: a is not defined
// finaltask
複製代碼

咱們看上面代碼,咱們在第一個then中輸出了一個未聲明的變量

輸出結果先走了catch而後走了最後一個then,第一個then中拋出錯誤並跳過了第二個then

也就是說若是咱們沒有處理這個錯誤(無catch)的話,就不會往下執行了

可參考下圖

promise的缺點之一就是沒法讓promise中斷,利用這個特性可讓Promise中斷執行,也算一種辦法吧

異步回調中拋錯catch捕捉不到

首先咱們看在Promise對象的處理器函數中直接拋出錯誤

const p = new Promise((resolve, reject)=>{
  throw new Error('這是一個錯誤')
});
p.catch((error)=>{ console.log(error) });
複製代碼

按照上述內容來看,在Promise對象的處理器函數中直接拋出錯誤,catch是能夠捕捉到的

在下面代碼,在Promise對象的處理器函數中模擬一個異步拋錯

const p = new Promise((resolve, reject)=>{
  setTimeout(()=>{ throw new Error('這是一個錯誤') }, 0)
});
p.catch((error)=>{ console.log(error) });
複製代碼

這種狀況catch是捕捉不到的,這是爲何呢?先想後看,再作不難

緣由

JS 事件循環列表有宏任務與微任務之分,setTimeOut是宏任務, promise是微任務,執行順序不一樣

那麼這段代碼的執行順序是:

  1. 代碼執行棧進入promise 觸發setTimeOut,setTimeOut回調函數入宏任務隊列
  2. 代碼執行promise的catch方法,入微任務隊列,此時setTimeOut回調尚未執行
  3. 執行棧檢查發現當前微任務隊列執行完畢,開始執行宏任務隊列
  4. 執行throw new Error('這是一個錯誤') 此時這個異常實際上是在promise外部拋出的

解決

使用try catch捕獲異常主動觸發reject

const p = new Promise((resolve, reject)=>{
  setTimeout(()=>{ 
    try{
       throw new Error('這是一個錯誤') 
    }catch(e){
       reject(e)
    }
 }, 0)
});
p.catch((error)=>{ console.log(error) });
複製代碼

手寫Promise—符合Promises/A+規範

爲何要手寫Promise

Promise源碼邏輯相對來講不算簡單,可能咱們只會使用,並不清楚其原理

本身實現一遍會加深咱們對Promise的理解,也能夠增強咱們JS的功底

更況且手寫實現Promise是一道前端經典的面試題,此處必然不用多說

Promises/A+

瞭解了Promise的基礎用法後,咱們來一步步倒推實現Promise

Promises/A+標準是一個開放、健全且通用的 JavaScript Promise 標準,由開發者制定,供開發者參考

不少Promise三方庫都是按照Promises/A+標準實現的

so,這次實現咱們嚴格Promises/A+標準,包括完成後咱們會使用開源社區提供的測試包來測試

簡單來講,測試經過的話,足以證實代碼符合Promises/A+標準,是合法的、徹底能夠上線提供給他人使用的

更多Promises/A+標準請看參考連接【6】【7】

構造方法核心基礎搭建

Promise的用法上面已經詳細講了,若是閱讀仔細的話,咱們會知道

  • Promise有三種狀態進行中 (Pending)、已完成 (Resolved/Fulfilled)和已失敗 (Rejected)
  • Promise是一個構造方法,實例化Promise時傳入一個函數做爲處理器
    • 處理器函數有兩個參數(resolve和reject)分別將結果變爲成功態和失敗態
    • Promise對象執行成功了要有一個結果,經過resolve傳遞出去,失敗的話失敗緣由經過reject傳遞出入
  • Promise的原型上定義着then方法

那麼根據咱們上面的這些已知需求咱們能夠寫出一個基礎的結構(寫法千千萬,喜歡class也能夠用class)

function Promise(executor) {
  // 狀態描述 pending resolved rejected
  this.state = "pending"
  // 成功結果
  this.value = undefined
  // 失敗緣由
  this.reason = undefined

  function resolve(value) {}

  function reject(reason) {}
}

Promise.prototype.then = function(onFulfilled, onRejected) {}
複製代碼

如上所示,咱們建立了一個Promise構造方法,state屬性保存了Promise對象的狀態,使用value屬性保存Promise對象執行成功的結果,失敗緣由使用reason屬性保存,這些命名徹底貼合Promises/A+標準

接着咱們在構造函數中建立了resolvereject兩個方法,而後在構造函數的原型上建立了一個then方法,以備待用

初始化實例executor當即執行

咱們知道,在建立一個Promise實例時,處理器函數(executor)是會當即執行的,因此咱們更改代碼

function Promise(executor) {
  this.state = "pending"
  this.value = undefined
  this.reason = undefined
	
  // 讓其處理器函數當即執行
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
  
  function resolve(value) {}
  function reject(reason) {}
}
複製代碼

resolve&reject回調實現

Promises/A+規範中規定,當Promise對象已經由pending狀態改變爲成功態(resolved)或失敗態(rejected)後不可再次更改狀態,也就是說成功或失敗後狀態不可更新已經凝固

所以咱們更新狀態時要判斷,若是當前狀態是pending(等待態)纔可更新,由此咱們來完善resolvereject方法

let _this = this

function resolve(value) {
  if (_this.state === "pending") {
    _this.value = value
    _this.state = "resolved"
  }
}

function reject(reason) {
  if (_this.state === "pending") {
    _this.reason = reason
    _this.state = "rejected"
  }
}
複製代碼

如上所示,首先咱們在Promise構造函數內部用變量_this託管構造函數的this

接着咱們在resolvereject函數中分別加入了判斷,由於只有當前態是pending纔可進行狀態更改操做

同時將成功結果和失敗緣由都保存到對應的屬性上

而後將state屬性置爲更新後的狀態

then方法基礎實現

接着咱們來簡單實現then方法

首先then方法有兩個回調,當Promise的狀態發生改變,成功或失敗會分別調用then方法的兩個回調

因此,then方法的實現看起來挺簡單,根據state狀態來調用不一樣的回調函數便可

Promise.prototype.then = function(onFulfilled, onRejected) {
  if (this.state === "resolved") {
    if (typeof onFulfilled === "function") {
      onFulfilled(this.value)
    }
  }
  if (this.state === "rejected") {
    if (typeof onRejected === "function") {
      onRejected(this.reason)
    }
  }
}
複製代碼

如上所示,因爲onFulfilled & onRejected兩個參數都不是必選參,因此咱們在判斷狀態後又判斷了參數類型,當參數不爲函數類型,就不執行,由於在Promises/A+規範中定義非函數類型可忽略

讓Promise支持異步

寫到這裏,咱們可能會以爲,咦?Promise實現起來也不難嘛,這麼快就有模有樣了,咱們來簡單測試下

let p = new Promise((resolve, reject) => {
  resolve(1)
})

p.then(data => console.log(data)) // 1
複製代碼

嗯,符合預期,再來試下異步代碼

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  },1000);
})

p.then(data => console.log(data)) // 無輸出
複製代碼

問題來了,Promise一個異步解決方案被咱們寫的不支持異步。。。

咱們來分析下,原本是等1000ms後執行then方法,運行上面代碼發現沒有結果,哪裏有問題呢?

setTimeout函數讓resolve變成了異步執行,有延遲,調用then方法的時候,此刻狀態仍是等待態(pending),then方法即沒有調用onFulfilled也沒有調用onRejected

嗯,清楚緣由咱們開始改造,若是是你,你會如何解決呢,此處可思考40秒,想一個可實施的大體思路

小提示: 能夠參考上文的發佈訂閱模式,若是40秒尚未思路,嗯,有待提升

|

|

-->爲了讓您小小活動一下左腦並活躍下氣氛,我也是煞費苦心( ps: 都看到這了,不點個贊鼓勵下就太沒勁了噻😄)

|

|

迴歸正題,咱們來解決這個問題

咱們能夠參照發佈訂閱模式,在執行then方法時若是還在等待態(pending),就把回調函數臨時寄存到隊列(就是一個數組)裏,當狀態發生改變時依次從數組中取出執行就行了

思路有了,咱們來實現下

首先,咱們要在構造方法中新增兩個Array類型的數組,用於存放成功和失敗的回調函數

function Promise(executor) {
  let _this = this
  this.state = "pending"
  this.value = undefined
  this.reason = undefined
  // 保存成功回調
  this.onResolvedCallbacks = []
  // 保存失敗回調
  this.onRejectedCallbacks = []
  // ...
}
複製代碼

咱們還須要改善then方法,在then方法執行時若是狀態是等待態,就將其回調函數存入對應數組

Promise.prototype.then = function(onFulfilled, onRejected) {
  // 新增等待態判斷,此時異步代碼還未走完,回調入數組隊列
  if (this.state === "pending") {
    if (typeof onFulfilled === "function") {
      this.onResolvedCallbacks.push(onFulfilled)
    }
    if (typeof onRejected === "function") {
      this.onRejectedCallbacks.push(onRejected)
    }
  }
  
  // 如下爲以前代碼塊
  if (this.state === "resolved") {
    if (typeof onFulfilled === "function") {
      onFulfilled(this.value)
    }
  }
  if (this.state === "rejected") {
    if (typeof onRejected === "function") {
      onRejected(this.reason)
    }
  }
}
複製代碼

如上所示,咱們改寫then方法,除了判斷成功態(resolved)、失敗態(rejected),咱們又加了一個等待態(pending)判斷,當狀態爲等待態時,異步代碼尚未走完,那麼咱們把對應的回調先存入準備好的數組中便可

最那麼,就差最後一步執行了,咱們在resolvereject方法中調用便可

function resolve(value) {
  if (_this.state === 'pending') {
    _this.value = value
    // 遍歷執行成功回調
    _this.onResolvedCallbacks.forEach(cb => cb(value))
    _this.state = 'resolved'
  }
}

function reject(reason) {
  if (_this.state === 'pending') {
    _this.reason = reason
    // 遍歷執行失敗回調
    _this.onRejectedCallbacks.forEach(cb => cb(reason))
    _this.state = 'rejected'
  }
}
複製代碼

到了這裏,咱們已經實現了Promise的異步解決,趕快測試下

實現Promise經典的鏈式調用

Promise的then方法能夠鏈式調用,這也是Promise的精華之一,在實現起來也算是比較複雜的地方了

首先咱們要理清楚then的需求是什麼,這須要仔細看Promises/A+規範中對then方法的返回值定義及Promise解決過程,固然你若是仔細閱讀了上文then方法的使用大概也清楚了,咱們在這裏再次總結下

  • 首先then 方法必須返回一個 promise 對象(劃重點)

  • 若是then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise對象返回

  • 若是then方法中沒有return語句,就返回一個用Undefined包裝的Promise對象

  • 若是then方法中出現異常,則調用失敗態方法(reject)跳轉到下一個then的onRejected

  • 若是then方法沒有傳入任何回調,則繼續向下傳遞(值穿透)

  • 若是then方法中返回了一個Promise對象,那就以這個對象爲準,返回它的結果

嗯,到此咱們需求已經明確,開始代碼實現

需求中說若是then方法沒有傳入任何回調,則繼續向下傳遞,可是每一個then中又返回一個新的Promise,也就是說當then方法中沒有回調時,咱們須要把接收到的值繼續向下傳遞,這個其實好辦,只須要在判斷回調參數不爲函數時咱們把他變成回調函數返回普通值便可

Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
  onRejected = typeof onRejected === "function" ? onRejected : err => { throw err }
	// ...
}
複製代碼

咱們上面then實現中,在每一個可執行處都加了參數是否爲函數的類型校驗,可是咱們這裏在then方法開頭統一作了校驗,就不須要參數校驗了

如今的then方法變成了

Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
  onRejected = typeof onRejected === "function" ? onRejected : err => { throw err }
	
  if (this.state === "pending") {
    this.onResolvedCallbacks.push(onFulfilled)
    this.onRejectedCallbacks.push(onRejected)
  }
  
  if (this.state === "resolved") {
    onFulfilled(this.value)
  }
  if (this.state === "rejected") {
    onRejected(this.reason)
  }
}
複製代碼

接着來

既然每一個thne都反回一個新的Promise,那麼咱們就先在then中建立一個Promise實例返回,開始改造

Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
  onRejected = typeof onRejected === "function" ? onRejected : err => { throw err }
  
  let promise2 = new Promise((resolve, reject) => {
   	if (this.state === "pending") {
      this.onResolvedCallbacks.push(onFulfilled)
      this.onRejectedCallbacks.push(onRejected)
    }

    if (this.state === "resolved") {
      onFulfilled(this.value)
    }
    if (this.state === "rejected") {
      onRejected(this.reason)
    }
  })
  return promise2
}
複製代碼

咱們在then方法中先實例化了一個Promise對象並返回,咱們把原來寫的代碼放到該實例的處理器函數中

接着在每一個執行函數處使用try..catch語法,try中resolve執行結果,catch中reject異常,原來的then方法中有resolved、rejected和pending三種邏輯判斷,以下

在resolved狀態判斷時,rejected和resolved邏輯一致

if (this.state === "resolved") {
  try {
    // 拿到返回值resolve出去
    let x = onFulfilled(this.value)
    resolve(x)
  } catch (e) {
    // catch捕獲異常reject拋出
    reject(e)
  }
}
複製代碼

pending狀態判斷,邏輯也和resolved類似,可是因爲此處爲了處理異步,咱們在這裏作了push操做,因此咱們push時在onFulfilled和onRejected回調外面再套一個回調作操做便可,都是JS慣用小套路,不過度解釋

if (this.state === "pending") {
  // push(onFulfilled)
  // push(()=>{ onFulfilled() })
  // 上面兩種執行效果一致,後者可在回調中加一些其餘功能,以下
  this.onResolvedCallbacks.push(() => {
    try {
      let x = onFulfilled(this.value)
      resolve(x)
    } catch (e) {
      reject(e)
    }
  })
  this.onRejectedCallbacks.push(() => {
    try {
      let x = onRejected(this.value)
      resolve(x)
    } catch (e) {
      reject(e)
    }
  })
}
複製代碼

再接下來咱們開始處理根據上一個then方法的返回值來生成新Promise對象,這塊邏輯複雜些,規範中能夠抽離出一個方法來作這件事,咱們來照作

/** * 解析then返回值與新Promise對象 * @param {Object} 新的Promise對象,就是咱們建立的promise2實例 * @param {*} x 上一個then的返回值 * @param {Function} resolve promise2處理器函數的resolve * @param {Function} reject promise2處理器函數的reject */
function resolvePromise(promise2, x, resolve, reject) {
  // ...
}
複製代碼

咱們來一步步分析完善resolvePromise函數

避免循環引用,當then的返回值與新生成的Promise對象爲同一個(引用地址相同),則拋出TypeError錯誤

例:

let promise2 = p.then(data => {
    return promise2;
})

// TypeError: Chaining cycle detected for promise #<Promise>
複製代碼

若是返回了本身的Promise對象,狀態永遠爲等待態(pending),再也沒法成爲resolved或是rejected,程序就死掉了,所以要先處理它

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    reject(new TypeError('請避免Promise循環引用'))
  }
}
複製代碼

判斷x類型,分狀況處理

當x是一個Promise,就執行它,成功即成功,失敗即失敗,若是x是一個對象或是函數,再進一步處理它,不然就是一個普通值

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    reject(new TypeError('請避免Promise循環引用'))
  }

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // 多是個對象或是函數
  } else {
    // 是個普通值
    resolve(x)
  }
}
複製代碼

若是x是個對象,嘗試將對象上的then方法取出來,此時若是報錯,那就將promise2轉爲失敗態

在這裏catch防止報錯是由於Promise有不少實現,假設另外一我的實現的Promise對象使用Object.defineProperty()在取值時拋錯,咱們能夠防止代碼出現bug

// resolvePromise方法內部片斷

if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  // 多是個對象或是函數
  try {
    // 嘗試取出then方法引用
    let then = x.then
  } catch (e) {
    reject(e)
  }
} else {
  // 是個普通值
  resolve(x)
}
複製代碼

若是對象中有then,且then是函數類型,就能夠認爲是一個Promise對象,以後,使用x做爲其this來調用執行then方法

// resolvePromise方法內部片斷

if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  // 多是個對象或是函數
  try {
    // 嘗試取出then方法引用
    let then = x.then
    if (typeof then === 'function') {
      // then是function,那麼執行Promise
      then.call(x, (y) => {
        resolve(y)
      }, (r) => {
        reject(r);
      })
    } else {
      resolve(x)
    }
  } catch (e) {
    reject(e)
  }
} else {
  // 是個普通值
  resolve(x)
}
複製代碼

此時,咱們還要考慮到一種狀況,若是Promise對象轉爲成功態或是失敗時傳入的仍是一個Promise對象,此時應該繼續執行,直到最後的Promise執行完,例以下面這種

Promise.resolve(1).then(data => {
  return new Promise((resolve,reject)=>{
    // resolve傳入的仍是Promise
    resolve(new Promise((resolve,reject)=>{
      resolve(2)
    }))
  })
})
複製代碼

解決這種狀況,咱們能夠採用遞歸,把調用resolve改寫成遞歸執行resolvePromise,這樣直到解析Promise成一個普通值纔會終止

// resolvePromise方法內部片斷
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  // 多是個對象或是函數
  try {
    let then = x.then
    if (typeof then === 'function') {
      then.call(x, (y) => {
        // 遞歸調用,傳入y如果Promise對象,繼續循環
        resolvePromise(promise2, y, resolve, reject)
      }, (r) => {
        reject(r)
      })
    } else {
      resolve(x)
    }
  } catch (e) {
    reject(e)
  }
} else {
  // 普通值結束遞歸
  resolve(x)
}
複製代碼

規範中定義,若是resolvePromise和rejectPromise都被調用,或者屢次調用同一個參數,第一個調用優先,任何進一步的調用都將被忽略,爲了讓成功和失敗只能調用一個,咱們接着完善,設定一個called來防止屢次調用

// resolvePromise方法內部片斷
let called
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  // 多是個對象或是函數
  try {
    let then = x.then
    if (typeof then === 'function') {
      then.call(x, (y) => {
        if (called) return
        called = true
        // 遞歸調用,傳入y如果Promise對象,繼續循環
        resolvePromise(promise2, y, resolve, reject)
      }, (r) => {
        if (called) return
        called = true
        reject(r)
      })
    } else {
      resolve(x)
    }
  } catch (e) {
    if (called) return
    called = true
    reject(e)
  }
} else {
  // 普通值結束遞歸
  resolve(x)
}
複製代碼

到此,咱們算是實現好了resolvePromise方法,咱們來調用它實現完整的then方法,在原來的原型方法then中咱們return了一個promise2,這個實例處理器函數的三種狀態判斷中把resolve處替換成resolvePromise方法便可

那麼,此時then方法實現完成了嗎?

固然尚未,咱們都知道,Promise中處理器函數是同步執行,而then方法是異步,可是咱們完成這個仍是同步

解決這個問題其實也很簡單,仿照市面上大多數Promise庫的作法,使用setTimeout模擬,咱們在then方法內執行處的全部地方使用setTimeout變爲異步便可(只是這樣作和瀏覽器自帶的Promises惟一的區別就是瀏覽器的Promise..then是微任務,咱們用setTimeout實現是宏任務),不過這也是大多數Promise庫的作法,以下

setTimeout(() => {
  try {
    let x = onFulfilled(value);
    resolvePromise(promise2, x, resolve, reject)
  } catch (e) {
    reject(e);
  }
},0)
複製代碼

如今咱們的終極版then方法就大功告成了

Promise.prototype.then = function(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
  
  let promise2 = new Promise((resolve, reject) => {
    // 等待態判斷,此時異步代碼還未走完,回調入數組隊列
    if (this.state === "pending") {
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      })

      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      })
    }
    if (this.state === "resolved") {
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
    if (this.state === "rejected") {
      setTimeout(() => {
        try {
          let x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
  })
  return promise2
}
複製代碼

catch實現

實現了最複雜的then方法後,catch實現很是簡單,一看就懂了

Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}
複製代碼

代碼測試

開源社區提供了一個包用於測試咱們的代碼是否符合Promises/A+規範:promises-aplus-tests

首先咱們要爲該測試包提供一個deferred鉤子,用於測試

以下,將下面代碼防止咱們的Promise.js文件末尾便可

// promises-aplus-tests測試
Promise.defer = Promise.deferred = function() {
  let defer = {}
  defer.promise = new Promise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}
try {
  module.exports = Promise
} catch (e) {}
複製代碼

接着,安裝這個包

npm install promises-aplus-tests -D
複製代碼

執行測試

npx promises-aplus-tests Promise.js
複製代碼

靜等片刻,若是控制檯沒有爆紅就是成功了,符合規範,如圖所示

完整代碼

篇幅已經很長了,後續還有其餘內容,因此就實現了比較核心的Promise及then和catch方法

其餘的resolve/reject/race/all等比較簡單,不在這裏描述了

給你們貼個我這邊Promise多個方法實現的地址,你們有興趣自行看代碼吧,註釋寫的很詳細了,也就大概200多行代碼

Promise優/缺

優勢

Promise用同步的方式寫異步的代碼,避免了層層嵌套的回調函數

Promise對象提供了統一的接口,使得控制異步操做更加容易

鏈式操做,能夠在then中繼續寫Promise對象並返回,而後繼續調用then來進行回調操做

缺點

Promise對象一旦新建就會當即執行,沒法中途取消

若不設置回調函數,Promise內部會拋出錯誤,不會流到外部

當處於pending狀態時,沒法得知當前處於哪一階段

用多了Promise後代碼一眼看上去都是promise的API,並且鏈式語法總以爲很差看,不優雅

Generator

Generator是協程在ES6的實現,最大的特色就是能夠交出函數的執行權

咱們能夠經過 yield 關鍵字,把函數的執行流掛起,爲改變執行流程提供了可能,從而爲異步編程提供解決方案

Generator的英文是生成器

想要了解生成器(Generator),仍是繞不過迭代器(Iterator)這個概念,咱們先來簡單介紹下

迭代器(Iterator)

Iterator簡介

迭代器是一種接口,也能夠說是一種規範

js中不一樣的數據類型如(Array/Object/Set)等等遍歷方式都各有不一樣,好比對象遍歷咱們會使用for..in..,數組可使用for循環/for..in../forEach等等

那麼有沒有統一的方式遍歷這些數據呢?這就是迭代器存在的意義,它能夠提供統一的遍歷數據的方式,只要在想要遍歷的數據結構中添加一個支持迭代器的屬性便可

Iterator語法

const obj = {
    [Symbol.iterator]:function(){}
}
複製代碼

[Symbol.iterator] 屬性名是固定的寫法,只要擁有了該屬性的對象,就可以用迭代器的方式進行遍歷

迭代器的遍歷方法是首先得到一個迭代器的指針,初始時該指針指向第一條數據以前

接着經過調用 next 方法,改變指針的指向,讓其指向下一條數據

每一次的 next 都會返回一個對象,該對象有兩個屬性

  • value 表明想要獲取的數據

  • done 布爾值,false表示當前指針指向的數據有值,true表示遍歷已經結束

Iterator詳解

在JS中,Array/Set/Map/String都默認支持迭代器

因爲數組和集合都支持迭代器,因此它們均可以用同一種方式來遍歷

es6中提供了一種新的循環方法叫作for-of,它實際上就是使用迭代器來進行遍歷

換句話說只有支持了迭代器的數據結構才能使用for-of循環

數組中使用迭代器遍歷

let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 獲取數組中的迭代器
console.log(it.next()) 	// { value: Object { num: 1 }, done: false }
console.log(it.next()) 	// { value: 2, done: false }
console.log(it.next()) 	// { value: 3, done: false }
console.log(it.next()) 	// { value: undefined, done: true }
複製代碼

數組是支持迭代器遍歷的,因此能夠直接獲取其迭代器,集合也是同樣

集合中使用迭代器遍歷

let list = new Set([1,3,2,3])
let it = list.entries() 	// 獲取set集合中自帶的的迭代器
console.log(it.next()) 	// { value: [ 1, 1 ], done: false }
console.log(it.next()) 	// { value: [ 3, 3 ], done: false }
console.log(it.next()) 	// { value: [ 2, 2 ], done: false }
console.log(it.next()) 	// { value: undefined, done: true }
複製代碼

集合與數組不一樣的是,咱們可使用Set中的entries方法獲取迭代器

Set集合中每次遍歷出來的值是一個數組,裏面的第一和第二個元素都是同樣的

自定義對象中使用迭代器遍歷

首先自定義的對象沒有迭代器屬性,因此不支持迭代器迭代,咱們也都知道for..of是沒法遍歷對象的,緣由就在這裏,由於for..of是使用迭代器迭代,因此對象不能用for..of

既然知道是由於自定義對象無迭代器屬性,那麼咱們能夠爲它加上Symbol.iterator這樣一個屬性,併爲它實現一個迭代器方法,以下

let obj = {
  name : 'tom',
  age : 18,
  gender : '男',
  intro : function(){
    console.log('my name is '+this.name)
  },
  [Symbol.iterator]:function(){
    let i = 0;
    // 獲取當前對象的全部屬性並造成一個數組
    let keys = Object.keys(this); 
    return {
      next: function(){
        return {
          // 外部每次執行next都能獲得數組中的第i個元素
          value:keys[i++], 
          // 若是數組的數據已經遍歷完則返回true
          done:i > keys.length 
        }
      }
    }
  }
}

for(let attr of obj){
  console.log(attr);
}
複製代碼

如上所示,加上[Symbol.iterator]這個迭代器屬性咱們自定義了一個迭代器方法,就可使用for..of方法了

Iterator做用

Iterator 的做用有三個:

  • 爲各類數據結構,提供一個統一的、簡便的訪問接口
  • 使得數據結構的成員可以按某種次序排列
  • ES6 創造了一種新的遍歷命令for..of循環,Iterator 接口主要供for..of消費

Iterator咱們就介紹到這裏,到這就理解上文Iterator參數是什麼了吧,就是表明一個有迭代器屬性的參數

初識Generator

Generator其實也是一個函數,只不過是一個特殊的函數

普通函數,你運行了這個函數,函數內部不會停,直到這個函數結束

Generator這個函數特殊之處就是,中間能夠停

Generator函數特色

function *generatorFn() {
  console.log("a");
  yield '1';
  console.log("b");
  yield '2'; 
  console.log("c");
  return '3';
}

let it = generatorFn()
it.next()
it.next()
it.next()
it.next()
複製代碼

上面這個示例就是一個Generator函數,首先咱們觀察它的特色,一個一個進行分析

  • 不一樣於普通函數,Generator函數在function後面,函數名以前有個*
    • *用來表示函數爲 Generator 函數
    • 寫法不少,function* fn()function*fn()function *fn()均可以
  • 函數內部有yield字段
    • yield用來定義函數內部的狀態,並讓出執行權
    • 這個關鍵字只能出如今生成器函數體內,可是生成器中也能夠沒有 yield 關鍵字,函數遇到 yield 的時候會暫停,並把 yield 後面的表達式結果拋出去
  • 調用後其函數返回值使用了next方法
    • 調用 Generator 函數和調用普通函數同樣,在函數名後面加上()便可
    • Generator 函數不會像普通函數同樣當即執行,而是返回一個指向內部狀態對象的指針
    • 因此要調用迭代器對象 Iterator 的 next 方法,指針就會從函數頭部或者上一次停下來的地方開始執行
    • next 方法其實就是將代碼的控制權交還給生成器函數

分析執行過程

接着咱們來分析它的執行過程,線來看它的打印結果,仍是上面那個例子

let it = generatorFn()
it.next()
// a
// {value: "1", done: false}

it.next()
// b
// {value: "1", done: false}

it.next()
// c
// {value: "1", done: true}

it.next()
// {value: undefined, done: true}
複製代碼

首先,Generator 函數執行,返回了一個指向內部狀態對象的指針,此時沒有任何輸出

第一次調用next方法,從 Generator 函數的頭部開始執行,先是打印了 a ,執行到yield就停下來,並將yield後邊表達式的值 '1',做爲返回對象的 value 屬性值,此時函數尚未執行完, 返回對象的 done 屬性值是 false

第二次調用next方法時,同上步

第三次調用next方法時,先是打印了 c ,而後執行了函數的返回操做,並將 return 後面的表達式的值,做爲返回對象的 value 屬性值,此時函數已經結束,因此 done 屬性值爲true

第四次調用next方法時, 此時函數已經執行完了,因此返回 value 屬性值是 undefined,done 屬性值是 true ,若是執行第三步時,沒有 return 語句的話,就直接返回 {value: undefined, done: true}

簡單的理解,Generator函數yield放到哪裏它就停到哪裏,調用時使用next方法踹一步就走一步

next參數傳遞

yield是有返回值的,next方法直接調用不傳入參數的時候,yield 表達式的返回值是 undefined

當 next 傳入參數的時候,該參數會做爲上一步yield的返回值

咱們經過示例來理解

function *geFn(){
  cosnole.log("start")
  let a = yield "1"
  
  console.log(a)
  let b = yield "2"
  
  console.log(b)
  let c = yield "3"
  
  console.log(c)
  return 4
}

let it = geFn()
it.next()
// start
// { value:1, done: false }

it1.next()
// undefined 未傳值,因此a=undefined
// { value:2, done: false }

it.next("hahaha")
// hahaha 傳值,因此b=hahaha
// { value:3, done: false }

it.next("omg")
// omg 傳值,因此c=omg
// {value: 4, done: true}
複製代碼

因爲 next 方法的參數表示上一個 yield 語句的返回值,因此第一次使用 next 方法時,不能帶有參數

V8引擎會直接忽略第一次使用 next 方法時的參數,只有從第二次使用 next 方法開始,參數纔是有效的

沒有接到傳值時,yield語句的返回值就是undefined,正如上面示例輸出那樣

經過 next 方法的參數,就有辦法在Generator函數開始運行以後,繼續向函數體內部注入值,這表明了咱們能夠在Generator函數運行的不一樣階段,從外部向內部注入不一樣的值,從而調整函數行爲

再次理解yield

咱們再來看一段代碼,幫助咱們理解yield

function *geFn(){
  console.log("start")
  let a = yield console.log("1")
  console.log(a)
  let b = yield console.log("2")
  console.log(b)
  return console.log("3")
}

let it = geFn()
it.next()
// start
// 1
// {value: 1, done: false}

it.next("我是a")
// 我是a
// 2
// {value: 2, done: false}

it.next("我是b")
// 我是b
// 3
// {value: 3, done: true}
複製代碼

經過next調用咱們能夠看到,第一次調用就輸出了start & 1 ,意味着yield中止時,後面代碼是執行了的

如上圖所示,若是將說yield比作一道牆,那麼牆右邊和上面是一塊,牆左邊和下面是一塊,這樣說應該夠直白了吧

for..of 遍歷Generator

上文咱們就知道了for...of內部實現就是在使用迭代器迭代,那麼for...of循環直接用在Generator遍歷器上豈不是完美

是的,它能夠自動遍歷Generator函數,並且此時再也不須要調用next方法,一旦next方法的返回對象的done屬性爲true,for...of循環就會停止,且不包含該返回對象

function *foo() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
  return 6
}
for (let v of foo()) {
  console.log(v)
}
// 1 2 3 4 5
複製代碼

yield* 表達式

yield命令後面加上星號,代表它返回的是一個遍歷器,這被稱爲yield*表達式

function *foo(){
  yield "foo1"
  yield "foo2"
}
function *bar(){
  yield "bar1"
  yield* foo()
  yield "bar2"
}
for(let val of bar()){
  console.log(val)
}

// bar1 foo1 foo2 bar2
複製代碼

yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器

function* gen1(){
  yield ["a", "b", "c"]
}
for(let val of gen1()){
  console.log(a)
}
// ["a", "b", "c"]

// ------------------- 上下分割

function* gen2(){
  yield* ["a", "b", "c"]
}
for(let val of gen2()){
  console.log(a)
}
// a b c
複製代碼

Generator中的return

return 方法返回給定值,並結束遍歷 Generator 函數

當 return 無值時,就返回 undefined,來看例子

function* foo(){
  yield 1
  yield 2
  yield 3
}

var f = foo()
f.next()
// {value: 1, done: false}

f.return("hahaha")
// 因爲調用了return方法,因此遍歷已結束,done變true
// {value: "hahaha", done: true}

f.next()
// {value: undefined, done: true}
複製代碼

Generator錯誤處理throw

throw方法能夠再 Generator 函數體外面拋出異常,再函數體內部捕獲,聽着是很好理解

這裏一不當心仍是挺容易入坑的,咱們來看幾個例子吧

function *foo(){
  try{
    yield 'hahaha'
  }catch(err){
    console.log('inside error: ' + err)
  }
}
var f = foo()
try{
    it.throw('this is err')
}catch(err){
    console.log('out error: ' + err)
}
複製代碼

上面代碼會輸出哪一個錯誤呢?

其實答案很簡單,上述代碼會輸出out error:this is err

由於調用throw的時候,咱們並無執行next方法,這個時候內部的try{}catch{}代碼都還沒執行,所以只會被外面捕捉

因此說,咱們只須要在調用throw以前,先調用一遍next,這個時候函數體內部已經執行了try{}catch{},那麼執行到throw時,內外都有錯誤捕捉,throw方法會先被內部捕捉,從而打印inside error:this is err

除此,throw方法會附帶執行下一個yield,咱們來看示例

var foo = function* foo(){
  try {
    yield console.log('1')
    yield console.log('2')
  } catch (e) {
    console.log('inside err')
  }
  yield console.log('3')
  yield console.log('4') 
}
var g = foo()
g.next()
g.throw()
g.next()
複製代碼

咱們來看上述代碼的執行過程

首先執行第一個next方法,進入try()catch(),輸出 1

接着,執行throw方法,內部捕捉到,輸出inside err,此時try()catch()代碼塊已經執行了catchtry()catch()代碼塊已經結束了,因此附帶執行一個yield會繼續向下找,因此再輸出3

最後執行next方法,輸出 4

最終輸出結果爲1 3 4

Generator擴充

在Generator開頭有一句話,不知道你們理解沒有

  • Generator是協程在ES6的實現,最大的特色就是能夠交出函數的執行權

什麼是協程?

這裏使用阮一峯老師的文章參考連接【8】中對協程的解釋並略帶修改及補充

進程和線程你們應該都清楚,那麼協程是什麼呢

不知道你們知不知道用戶空間線程,其實就是一種由程序員本身寫程序來管理他的調度的線程,對內核來講不可見

協程(coroutine),能夠理解就是一種「用戶空間線程」,也可理解爲多個「線程」相互協做,完成異步任務

因爲線程是操做系統的最小執行單元,所以也能夠得出,協程是基於線程實現的,不過它要比線程要輕不少

協程,有幾個特色:

  • 協同,由於是由程序員本身寫的調度策略,其經過協做而不是搶佔來進行切換
  • 在用戶態完成建立,切換和銷燬
  • 編程角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢復(resume)機制

它的運行流程以下

  • 協程A開始執行
  • 協程A執行到一半,暫停執行,執行的權利轉交給協程B。
  • 一段時間後B交還執行權
  • 協程A重得執行權,繼續執行

上面的協程A就是一個異步任務,由於在執行過程當中執行權被B搶了,被迫分紅兩步完成

舉例來講,讀取文件的協程寫法以下

function asnycJob() {
  // ...其餘代碼
  var f = yield readFile(fileA)
  // ...其餘代碼
}
複製代碼

上面代碼的函數 asyncJob 是一個協程,其中的 yield 命令,它表示執行到此處,執行權將交給其餘協程,也就是說,yield命令是異步兩個階段的分界線

協程遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行,它的最大優勢,就是代碼的寫法很是像同步操做,只多了一個yield命令

Generator與協程

JS是單線程的,ES6中的Generator的實現,相似於開了多線程,可是依然同時只能進行一個線程,不過能夠切換

就像汽車在公路上行駛,js公路只是單行道(主線程),可是有不少車道(輔助線程)均可以匯入車流(異步任務完成後回調進入主線程的任務隊列)

而 Generator 把js公路變成了多車道(協程實現),可是同一時間只有一個車道上的車能開(因此依然是單線程),不過能夠自由變道(移交控制權)

Generator之Thunk函數

thunk函數的誕生源於一個編譯器設計的問題:求值策略,即函數的參數到底應該什麼時候求值

var x = 1
function fn(n) {
    return n * 10
}
fn(x + 5)
複製代碼

如上所示,其中fn方法調用時x+5這個表達式應該何時求值,有兩種思路

  • 傳值調用(call by value),先計算x+5的值,再將這個值 6 傳入函數fn,例如c語言,這種作法的好處是實現比較簡單,可是有可能會形成性能損失(例如一個函數傳入了兩個參數,第二個參數是一個表達式,可是函數體內沒有用到這個參數,那麼先計算出值就會損耗性能且無心義)
  • 傳名調用(call by name),即直接將表達式x+5傳入函數體,只在用到它的時候求值

Thunk 函數的定義,就是傳名調用的一種實現策略,用來替換某個表達式,實現思路其實也很簡單

先將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體,就像下面這樣

function fn(m){
  return m * 2    
}
fn(x + 5)

// thunk實現思路
var thunk = function () {
  return x + 5
}

function fn(thunk){
  return thunk() * 2
}
複製代碼

JS是傳值調用,它的Thunck函數含義有所不一樣

在JS中,Thunk函數替換的不是表達式,是對函數珂里化的一種運用,簡單來講,就是把是多參數函數替換成一個只接受回調函數做爲參數的單參數函數,咱們來看下它的簡單實現

fs.readFile(fileName, callback)

const Thunk = function(fn){
  return function(...args){
    return function(callback){
      return fn.call(this,...args,callback)
    }
  }
}

// 使用上面的Thunk轉化器,生成fs.readFile的Thunk函數
var readFileThunk = Thunk(fs.readFile)
readFileThunk(fileName)(callback)
複製代碼

若是在生產環境要使用Thunk函數的話,使用Thunkify模塊就能夠,其實它核心源碼就是上面咱們寫的Thunk,Thunkify裏多了一個檢查機制而已,比較簡單,可自行百度Thunkify模塊瞭解

Thunk這東西在ES6前其實沒有太大用處,可是在Generator函數出來後,Thunk函數就能夠派上用場了,它能夠用於Generator函數的自動流程管理,接收和交換程序的執行權

咱們來實現一個基於Thunk函數的Generator自動執行器

// 基於Thunk函數的Genertor函數自動執行器
function run(fn) {
  let gen = fn()
  function next(err, data) {
    // 將指針移動到Generator函數的下一步
    let result = gen.next(data)
    // 判斷是否結束
    if (result.done) return
    // 遞歸,把next放進.value中
    result.value(next)
  }
  next()
}

// 模擬異步方法
let sleep = function(n, callback) {
  setTimeout(() => {
    console.log(n)
    callback && callback(n)
  }, n)
}

// 模擬異步方法進行Thunk轉換
let sleepThunk = Thunk(sleep)

// Generator函數
let gen = function*() {
  let f1 = yield sleepThunk(1000)
  let f2 = yield sleepThunk(1500)
  // ...
  let fn = yield sleepThunk(2000)
}

// 調用Genertor函數自動執行器
run(gen)
複製代碼

上面代碼的 run 函數,就是一個 Generator 函數的自動執行器,內部的 next 函數就是 Thunk 的回調函數

next 函數先將指針移到 Generator 函數的下一步(gen.next 方法)

而後判斷 Generator 函數是否結束(result.done 屬性)

若是沒結束,就將 next 函數再傳入 Thunk 函數(result.value 屬性),不然就直接退出

代碼中模擬了一個異步操做sleep方法,並將其轉化爲了Thunk方法(使用上文咱們實現的那個簡易版Thunk)

函數 gen 封裝了 n 個異步操做,只要執行 run 函數,這些操做就會自動完成

這樣一來,異步操做不只能夠寫得像同步操做,並且一行代碼就能夠執行,極其方便

不過相信你們也看到了,這種自動執行器傳入的Generator函數,yield方法後面必須是一個Thunk 函數

--------👇--------

Thunk就簡單介紹到這裏了,更多Thunk相關推薦看阮一峯文參考連接【9】

咱們只須要明白Thunk是什麼,它和Generator有什麼關係就能夠

Generator之co函數庫

co 函數庫是著名程序員 TJ Holowaychuk 於2013年6月發佈的一個小工具,用於 Generator 函數的自動執行

co函數庫源碼傳送門

co 函數庫其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個庫,因此說使用 co 的前提條件是,Generator 函數的 yield 命令後面,只能是 Thunk 函數或 Promise 對象

co函數會返回一個Promise,因此咱們能夠後接then等方法

基於Thunk函數的自動執行器上面介紹了下,那麼基於Promise的其實也差很少,咱們簡單實現下

// 基於Promise函數的Genertor函數自動執行器
function run(gen) {
  let g = gen()

  function next(data) {
    // 將指針移動到Generator函數的下一步
    let result = g.next(data)
    // 判斷是否結束,結束返回value,value是一個Promise
    if (result.done) return result.value
    // 遞歸
    result.value.then(data => {
      next(data)
    })
  }
  next()
}

// 模擬異步方法進行Promise轉換
let sleepPromise = function(n) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => {
      console.log(n)
      resolve(n)
    }, n)
  })
}

// Generator函數
let gen = function*() {
  let f1 = yield sleepPromise(1000)
  let f2 = yield sleepPromise(1500)
  // ...
  let fn = yield sleepPromise(2000)
}

// 調用Genertor函數自動執行器
run(gen)
複製代碼

如上代碼,和Thunk函數那裏區別就是yield後面一個跟Thunk函數,一個跟Promise對象

若是Thunk自執行器你理解了,Promise使用也ok的話,這塊代碼看看就懂了,也沒啥解釋的

接下來咱們來看看co庫的源碼

co函數庫的源碼也很簡單,只有幾十行代碼

首先,co 函數接受 Generator 函數做爲參數,返回一個 Promise 對象

function co(gen) {
  var ctx = this
  return new Promise(function(resolve, reject) {
  })
}
複製代碼

在返回的 Promise 對象裏面,co 先檢查參數 gen 是否爲 Generator 函數

若是是,就執行該函數,獲得一個內部指針對象

若是不是就返回,並將 Promise 對象的狀態改成 resolved

function co(gen) {
  var ctx = this

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx)
    if (!gen || typeof gen.next !== 'function') return resolve(gen)
  })
}
複製代碼

接着,co 將 Generator 函數的內部指針對象的 next 方法,包裝成 onFulefilled 函數

主要是爲了可以捕捉拋出的錯誤

function co(gen) {
  var ctx = this

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx)
    if (!gen || typeof gen.next !== 'function') return resolve(gen)

    onFulfilled()
    function onFulfilled(res) {
      var ret
      try {
        ret = gen.next(res)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }    
  })
}
複製代碼

最後,就是關鍵的 next 函數,它會反覆調用自身

function next(ret) {
  if (ret.done) return resolve(ret.value)
  var value = toPromise.call(ctx, ret.value)
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected)
  return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, but the following object was passed: "' + String(ret.value) + '"'))
    }
})
複製代碼

next方法中,第一行,檢查當前是否爲 Generator 函數的最後一步,若是是就返回

第二行,確保每一步的返回值,是 Promise 對象

第三行,使用 then 方法,爲返回值加上回調函數,而後經過 onFulfilled 函數再次調用 next 函數

第四行,在參數不符合要求的狀況下(參數非 Thunk 函數和 Promise 對象),將 Promise 對象的狀態改成 rejected,從而終止執行

co 支持併發的異步操做,即容許某些操做同時進行,等到它們所有完成,才進行下一步,咱們能夠併發的操做放在數組或對象裏面,以下

// 數組的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ]
  console.log(res)
}).catch(onerror)

// 對象的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  }
  console.log(res)
}).catch(onerror)
複製代碼

-------👇-------

以上就是co的內容了,這裏說起只是爲了讓你們瞭解co這種函數庫,雖然目前用的很少,可是對咱們理解Generator有幫助,即便這裏有些迷糊,也無傷大雅,知道co是什麼,co的自動執行原理大概是怎麼實現的就行

這塊和Thunk同樣,也是參考阮一峯老師的文章,因此有興趣的話能夠看下參考連接【10】

Generator優/缺

優勢

優雅的流程控制方式,可讓函數可中斷執行,在某些特殊需求裏仍是很實用的

使用過React-dva的同窗可能會更有感觸一些

以前Node的koa框架也用Generator,不事後來被async/await替代了

缺點

Generator 函數的執行必須靠執行器,因此纔有了 co 函數庫,但co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,只針對異步處理來講,仍是不太方便

Async/Await

Async和Await簡介

ES2017 標準引入了 async 函數,使得異步操做變得更加方便

JS異步編程解決方案的歷程,從經典的回調函數到事件監聽,再到 Promise ,再到 Generator ,再到咱們要說的 Async/Await ,可謂艱辛

Async/Await 的出現,被不少人認爲是JS異步操做的最終且最優雅的解決方案

Async/Await 你們都常用,也都知道它是 Generator 的語法糖

其實我以爲 Async/Await = Generator + Promise 這個解釋更適合

async 是異步的意思,而 awaitasync wait 的簡寫,即異步等待

因此從語義上就很好理解 async 用於聲明一個 function 是異步的,await 用於等待一個異步方法執行完成

另外 await 只能出如今 async 函數中

閒聊至此,接下來仍是簡單介紹下使用

Async在作什麼

咱們來看一個例子理解

async function test() {
  return "this is async"
}
const res = test()
console.log(res)
// Promise {<resolved>: "this is async"}
複製代碼

能夠看到,輸出的是一個Promise對象

因此,async 函數返回的是一個 Promise 對象,若是在 async 函數中直接 return 一個直接量,async 會把這個直接量經過 PromIse.resolve() 封裝成Promise對象返回

既然 async 返回一個 Promise,那麼咱們也能夠用 then 鏈來處理這個 Promise 對象,以下

test().then(res=>{
  console.log(res)
})
複製代碼

Await在等待什麼

咱們常說await 是在等待一個異步完成, 其實按照語法說明, await 等待的是一個表達式,這個表達式的計算結果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定,啥都行)

  • await 後面不是Promise對象,直接執行
  • await 後面是Promise對象會阻塞後面的代碼,Promise對象 resolve,而後獲得 resolve 的值,做爲 await 表達式的運算結果
  • await 只能在 async 函數中使用

使用比較簡單,你們也常常用就很少說了

簡單說一下爲什 await 必需要在 async 函數中使用

其實很簡單, await 會阻塞後面代碼,若是容許咱們直接使用 await 的話,假如咱們使用await等待一個消耗時間比較長的異步請求,那代碼直接就阻塞不往下執行了,只能等待 await 拿到結果纔會執行下面的代碼,那不亂套了

async 函數調用不會形成阻塞,由於它內部全部的阻塞都被封裝在一個 Promise 對象中異步執行,因此才規定 await 必須在 async 函數中

處理異常

promise正常resolve,那麼await會返回這個結果,可是在reject的狀況下會拋出一個錯誤

因此咱們直接把 await 代碼塊寫到 try()catch() 中捕獲錯誤便可

async function fn(){
  try{
    let res = await ajax()
    console.log(res)
  }catch(err){
    console.log(err)
  }
}
複製代碼

沒有對比沒有傷害

咱們常常會遇到這種業務,多個請求,每一個請求依賴於上一個請求的結果

咱們用setTimeout模擬異步操做,用Promise和Async/Await分別來實現下

function analogAsync(n) {
  return new Promise(resolve => {
    setTimeout(() => resolve(n + 500), n)
  })
}

function fn1(n) {
  console.log(`step1 with ${n}`)
  return analogAsync(n)
}

function fn2(n) {
  console.log(`step2 with ${n}`)
  return analogAsync(n)
}

function fn3(n) {
  console.log(`step3 with ${n}`)
  return analogAsync(n)
}
複製代碼

使用Promise

function fn(){
  let time1 = 0
  fn1(time1)
    .then((time2) => fn2(time2))
    .then((time3) => fn3(time3))&emsp;&emsp;
    .then((res) => {
    	console.log(`result is ${res}`)
  	})
}

fn()
複製代碼

使用Async/Await

async function fn() {
  let time1 = 0
  let time2 = await fn1(time1)
  let time3 = await fn2(time2)
  let res = await fn3(time3)
  console.log(`result is ${res}`)
}

fn()
複製代碼

輸出結果和上面用Promise實現是同樣的,但這個 aaync/await 代碼結構看起來清晰得多,幾乎跟同步寫法同樣,十分優雅

咱們再來看下面這個小例子

// Generator
function* gen(){
  let f1 = yield ajax()
  let f2 = yield ajax()
}
gen()

// async/await
async function asyncAjax(){
  let f1 = await ajax()
  let f2 = await ajax()
}
asyncAjax()
複製代碼

這兩塊代碼看着是否是幾乎同樣

上面函數爲Generator函數執行兩個ajax,下面函數爲async/await執行

比較可發現,兩個函數實際上是同樣的,async 不過是把Generator函數的 * 號換成 asyncyield 換成 await

那麼這兩個函數在調用時,Generator函數須要手動調用 next 方法或者使用 co 函數庫纔可執行,而下面的async 函數直接就按順序執行完成了,使用很是方便

異步編程追求的是,讓它更像同步編程, Async/Await 完美詮釋了這一點

到這裏咱們其實就不難看出 Async/Await 已經完虐了 GeneratorPromise

Async/Await優/缺

優勢

內置執行器, Generator 函數的執行必須靠執行器,因此纔有了 co 函數庫,而 async 函數自帶執行器,也就是說,async 函數的執行,與普通函數如出一轍,只要一行

更好的語義,asyncawait,比起 *yield,語義更清楚了,async 表示函數裏有異步操做,await 表示緊跟在後面的表達式須要等待結果

更廣的適用性,co 函數庫約定,yield 命令後面只能是 Thunk 函數或 Promise 對象,而 async 函數的 await 命令後面,能夠跟 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)

缺點

濫用 await 可能會致使性能問題,由於 await 會阻塞代碼,也許以後的異步代碼並不依賴於前者,但仍然須要等待前者完成,致使代碼失去了併發性

異步解決方案對比

別看了,我沒有總結對比

其實相對來講已經寫的很詳細了,能講出來的纔算是本身的,你們可根據每種方案列出的優缺點加上本身的理解作個對比或着說總結,畢竟你都看到這了,也不妄花費這麼長時間來閱讀這兩萬字的幹帖子,總歸要有些收穫的

寫在最後

水平有限,歡迎指錯

碼字不易,你們有收穫別忘了點個贊鼓勵下

搜索【不正經的前端】或直接掃碼能夠關注公衆號看到更多的精彩文章,也有一些羣友提供的學習視頻、資源乾貨什麼的免費拿

也能夠直接加我微信,進交流羣學習交流

參考

  1. Javascript異步編程的4種方法-阮一峯
  2. Promise-MDN
  3. iterable-廖雪峯
  4. What is Promise.try, and why does it matter?
  5. 什麼是Promise.try,爲何它這麼重要?-參考4譯
  6. Promise/A+規範-英原文
  7. Promise/A+規範-中文譯
  8. Generator 函數的含義與用法-阮一峯
  9. Thunk 函數的含義和用法-阮一峯
  10. co 函數庫的含義和用法-阮一峯
相關文章
相關標籤/搜索