js 異步編程,async 函數與 Promise、Generator 函數的比較

 

1Promise

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。es6

 

特色:編程

1)對象的狀態不受外界影響。數組

Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。promise

2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。服務器

缺點:數據結構

1)沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。app

2)若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。異步

3)當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。async

 

Promise 新建後就會當即執行。異步編程

then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行。

 

對於Promise的嵌套,例如p1中resolve或者reject p2,那麼p1的狀態取決於p2.

 

Promise.prototype.then

做用是爲 Promise 實例添加狀態改變時的回調函數。

第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。

then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。

所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。

採用鏈式的then,能夠指定一組按照次序調用的回調函數。

執行時間:會在本輪「事件循環」(event loop)的結束時執行。

 

Promise.prototype.catch

  catch是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。

  若是 Promise 狀態已經變成resolved,再拋出錯誤是無效的。

  Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。

 

Promise.prototype.finally

用於指定無論 Promise 對象最後狀態如何,都會執行的操做。

無論promise最後的狀態,在執行完then或catch指定的回調函數之後,都會執行finally方法指定的回調函數。

例子,服務器使用 Promise 處理請求,而後使用finally方法關掉服務器。

 

Promise.all

用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

若是不是 Promise 實例,就會先調用Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。

Promise.all方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。

兩種狀況:

1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。

2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

注意,若是做爲參數的 Promise 實例,本身定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()的catch方法。

 

Promise.race

一樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。

若是不是 Promise 實例,就會先調用Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。

只要多個實例之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

例子,若是指定時間內沒有得到結果,就將 Promise 的狀態變爲reject,不然變爲resolve。(請求超時)

 

Promise.resolve

將現有對象轉爲 Promise 對象,Promise.resolve方法就起到這個做用。

四種狀況:

1)參數是一個 Promise 實例

將不作任何修改、原封不動地返回這個實例。

  (2)參數是一個thenable對象

thenable對象指的是具備then方法的對象,會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。

  (3)參數不是具備then方法的對象,或根本就不是對象

若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved。

Promise.resolve方法的參數,會同時傳給回調函數。

  (4)不帶有任何參數

容許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。

因此,若是但願獲得一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法。

須要注意的是,當即resolve的 Promise 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。

 

Promise.reject

也會返回一個新的 Promise 實例,該實例的狀態爲rejected

注意,Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。

 

Promise.try

實際開發中,常常遇到一種狀況:

不知道或者不想區分,函數f是同步函數仍是異步操做,可是想用 Promise 來處理它。

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

通常就會採用下面的寫法:

  

Promise.resolve().then(f)

上面的寫法有一個缺點,就是若是f是同步函數,那麼它會在本輪事件循環的末尾執行。

鑑於這是一個很常見的需求,因此如今有一個提案,提供Promise.try方法替代上面的寫法。

 

 

 

2generator/yield

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。

多種理解角度:

語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。

形式上,Generator 函數是一個普通函數,可是有兩個特徵。

1)function關鍵字與函數名之間有一個星號;

2)函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。

Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。

不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是遍歷器對象(Iterator Object)。

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。

yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

 

注意

yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。(定義的時候就會報錯,不會等到next時)

yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。

yield表達式用做函數參數或放在賦值表達式的右邊,能夠不加括號。

yield與return

類似之處在於,都能返回緊跟在語句後面的那個表達式的值。

區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。

 

與 Iterator 接口的關係

任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。

因爲 Generator 函數就是遍歷器生成函數,所以能夠把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具備 Iterator 接口。

 var myIterable = {};

   myIterable[Symbol.iterator] = function* () { yield 1;  yield 2;  yield 3; };

   [...myIterable] // [1, 2, 3]

Generator 函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。

function* gen(){ … }

 var g = gen();

 g[Symbol.iterator]() === g

next 方法的參數

yield表達式自己沒有返回值,或者說老是返回undefined。

next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值。(第一個next方法的參數是無效的)

 

for...of 循環 

for...of循環能夠自動遍歷 Generator 函數時生成的Iterator對象,且此時再也不須要調用next方法。

注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會停止,且不包含該返回對象。(return語句,不包括在for...of循環之中)

利用for...of循環,能夠寫出遍歷任意對象(object)的方法。

原生的 JavaScript 對象沒有遍歷接口,沒法使用for...of循環,經過 Generator 函數爲它加上這個接口,就能夠用了。

 

for...of循環、擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。

 

Generator.prototype.throw

Generator 函數返回的遍歷器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲(只能捕獲一次)。

注意:遍歷器對象的throw與全局對象的throw方法是不同的。

  若是Generator函數中沒有定義catch語句,就會被外部的catch捕獲。

 

Generator.prototype.return

返回給定的值,而且終結遍歷 Generator 函數。

遍歷器對象調用return方法後,返回值的value屬性就是return方法的參數,返回值的done屬性爲true。

而且,Generator 函數的遍歷就終止了,之後再調用next方法,done屬性老是返回true。

注意:若是 Generator 函數內部有try...finally代碼塊,那麼return方法會推遲到finally代碼塊執行完再執行。(即最後才執行return方法)

 

next()、throw()、return()的共同點

next()是將yield表達式替換成一個值。

throw()是將yield表達式替換成一個throw語句

return()是將yield表達式替換成一個return語句。

 

yield* 表達式

若是在 Generator 函數內部,調用另外一個 Generator 函數,默認狀況下是沒有效果的。

這個就須要用到yield*表達式,用來在一個 Generator 函數裏面執行另外一個 Generator 函數。

從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield表達式後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield*表達式。

yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of循環。

反之,在有return語句時,則須要用var value = yield* iterator的形式獲取return語句的值。

若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。

實際上,任何數據結構只要有 Iterator 接口,就能夠被yield*遍歷。

 

yield*命令能夠很方便地取出嵌套數組的全部成員。

 

做爲對象屬性的 Generator 函數

  let obj = {

    * myGeneratorMethod() {  ··· }

  };

 

Generator 函數的this

Generator 函數在this對象上面添加了一個屬性a,可是生成的遍歷器對象拿不到這個屬性。

new命令跟Generator 函數一塊兒使用,結果報錯,由於Generator 函數不是構造函數。

那麼,有沒有辦法讓 Generator 函數返回一個正常的對象實例,既能夠用next方法,又能夠得到正常的this?

使用call或者apply方法,動態的綁定Generator 函數內的this對象,使用Generator 函數的原型對象便可。

應用

1)異步操做的同步化表達

使用同步的方式書寫函數,在異步的回調中調用next方法

2)控制流管理 

若是有一個多步操做很是耗時,採用回調函數,會出現多層嵌套。

使用Promise會進一步改善代碼流程,使用Generator 函數還能夠進一步改善代碼運行流程。而後,使用一個函數,按次序自動執行全部步驟。

3)部署 Iterator 接口

利用 Generator 函數,能夠在任意對象上部署 Iterator 接口。

4)做爲數據結構

Generator 能夠看做是數據結構,更確切地說,能夠看做是一個數組結構,

由於 Generator 函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。

 

Generator 函數的異步應用

傳統方法

回調函數

事件監聽

發佈/訂閱

Promise 對象

Generator 函數將 JavaScript 異步編程帶入了一個全新的階段。

基本概念

Generator 函數的數據交換和錯誤處理

Generator 函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。

除此以外,它還有兩個特性,使它能夠做爲異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。

 

Thunk 函數

Thunk 函數是自動執行 Generator 函數的一種方法。

含義

編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk 函數。

 

JavaScript 語言的 Thunk 函數 

JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不一樣。在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數做爲參數的單參數函數。

 

 

3async/await

最關心的問題怎麼將異步變成同步?

(同步就使用 await ,而 await 後面接一個 Promise 便可,會同步的等待而且拿到 Promise的結果)

含義

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

async 函數是什麼?一句話,它就是 Generator 函數的語法糖。

async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。

 

async函數對 Generator 函數的改進,體如今如下四點:

1)內置執行器

Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。

也就是說,async函數的執行,與普通函數如出一轍,只要一行。

2)更好的語義

async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

3)更廣的適用性

co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,

async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

(4)返回值是 Promise has of

async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。

 

基本用法

async函數返回一個 Promise 對象,可使用then方法添加回調函數。

 

語法 

async函數的語法規則整體上比較簡單,難點是錯誤處理機制。

返回 Promise 對象

async函數返回一個 Promise 對象。

async函數內部return語句返回的值,會成爲then方法回調函數的參數。

async函數內部拋出錯誤,會致使返回的 Promise 對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。

 

Promise 對象的狀態變化

async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。

也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。

 

await 命令

正常狀況下,await命令後面是一個 Promise 對象。若是不是,會被轉成一個當即resolve的 Promise 對象。

await命令後面的 Promise 對象若是變爲reject狀態,則reject的參數會被catch方法的回調函數接收到。

只要一個await語句後面的 Promise 變爲reject,那麼整個async函數都會中斷執行。(若是不但願中斷執行能夠try...catch)

另外一種方法是await後面的 Promise 對象再跟一個catch方法,處理前面可能出現的錯誤。

 

錯誤處理

若是await後面的異步操做出錯,那麼等同於async函數返回的 Promise 對象被reject

防止出錯的方法,也是將其放在try...catch代碼塊之中。

或者爲await後面的Promise對象添加catch方法。

 

使用注意點

1、await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catch代碼塊中。

二、多個await命令後面的異步操做,若是不存在繼發關係(比較耗時),最好讓它們同時觸發。

三、await命令只能用在async函數之中,若是用在普通函數,就會報錯。

async 函數的實現原理

是將 Generator 函數和自動執行器,包裝在一個函數裏。

 

 async 函數與 PromiseGenerator 函數的比較

Promise 的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是 Promise 的 API(then、catch等等),操做自己的語義反而不容易看出來。

Generator 函數語義比 Promise 寫法更清晰,這個寫法的問題在於,必須有一個任務運行器,自動執行 Generator 函數。

Async 函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。

它將 Generator 寫法中的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。

若是使用 Generator 寫法,自動執行器須要用戶本身提供。

 

 

特別說明:本文中的不少內容都是參考阮一峯老師的ECMAScript 6 入門,若有轉載請註明出處。

相關文章
相關標籤/搜索