下篇 JavaScript異步進化史:Callbacks,Promises,Async

原文 The Evaluation of Async JavaScript :From Callbacks, to Promises, to Async/Await - (https://tylermcginnis.com/async-javascript-from-callbacks-to-promises-to-async-await/)

Promises

你有沒有曾經在沒有預訂的情況下去過一個繁忙的餐廳?當這種情況發生時,餐廳需要一種方法在有空桌時與你聯繫。一般而言,他們只會取得你的名字並在有空桌時大聲呼喊。然後,自然而然地,他們會開始想要變通。一個解決方案是,他們就會得到你的電話號碼而不是你的名字,然後在有空桌時給你發信息。這可以使你能夠散步到服務員們大喊的範圍以外,但更重要的是,如果他們想的話,他們可以給你的電話發廣告。聽起來有點熟?是的!好吧,也許也不是。這是回調的隱喻!將你的號碼提供給餐廳就像給第三方庫提供回調函數一樣。你希望餐廳在有空桌時給你發短信,就像你希望第三方服務如他們所說的那樣會在何時以及如何調用你的函數一樣。一旦你的號碼或回調函數落入他們手中,你就失去了所有控制權。

慶幸的是,存在另一種解決方案。一個被設計成允許你保留所有控制權的方案。你甚至可能以前都體驗過了,就是他們給你的小蜂鳴器。你知道的,就是這個東西:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

餐廳用蜂鳴器


如果你之前從未使用過這玩意兒,那麼這個概念很簡單。他們沒有取你的名字或號碼,而是給你這個設備。當設備開始嗡嗡作響併發光時,意味著你的桌子已經準備好了。當你在等待空桌時,你仍然可以做任何你想做的事,但現在你不必失去任何東西。事實上,恰恰相反。他們反而必須給你一些東西,這裡沒有控制的轉化。

蜂鳴器將始終處於三種不同狀態之一:pending(待決),fulfilled(已實現)或rejected(已駁回)。

  • pending是默認的初始狀態。當他們給你蜂鳴器時,它就處於這種狀態。
  • fulfilled是蜂鳴器閃爍並且你的桌子已準備就緒時所處的狀態。
  • rejected是當出現問題時蜂鳴器所處的狀態。也許餐廳即將關閉,或者他們忘了有人在晚上包下了餐廳。

再次強調,應該記住的重點是,你這個蜂鳴器的接收人,擁有所有的控制權。如果蜂鳴器轉變為fulfilled狀態,你可以去你的桌子坐下開始點餐,但是如果你想忽略它,這很酷,當然你也是可以這樣做的。如果它轉變為rejected狀態,那很糟糕,但你可以去別的地方吃飯。如果沒有任何事情發生並且它一直處於pending狀態,你會永遠都吃不到東西,但是實際上你也並沒有失去任何東西。

現在你已成為餐廳蜂鳴器的主人,讓我們將這些知識應用到相關的事情上。

如果給餐廳你的號碼就像給他們一個回調函數,那麼接受這個小小的蜂鳴器就像收到他們所謂的“承諾”。

跟往常一樣,讓我們從為什麼開始吧。為什麼Promises存在?因為它們的存在使得複雜的異步請求更易於管理。正如蜂鳴器一樣,Promise可以處於三種狀態之一,pending,fulfilled或rejected。與蜂鳴器不同的是,蜂鳴器代表餐廳桌子的狀態,而Promise代表異步請求的狀態。

如果異步請求仍在進行中,則Promise將處於pending狀態。如果異步請求成功,則Promise將更改為fulfilled的狀態。如果異步請求失敗,則Promise將更改為rejected狀態。蜂鳴器這個比喻很有意義,對嗎?

既然你已經理解了Promise存在的原因以及它們的不同狀態,那麼我們還需要去回答三個問題。

  • 你如何創建一個Promise?
  • 你如何改變Promise的狀態?
  • 你如何監聽Promise的狀態變化?

1)你如何創建一個Promise?

這裡有一個最直接的做法,你可以new一個Promise對象的實例:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

2)你如何改變Promise的狀態?

Promise構造函數使用一個回調函數作為參數,這個函數用來傳遞兩個參數,resolve和reject。

resolve:一個允許你將promise的狀態改變為fulfilled的函數

reject:一個允許你將promise的狀態改變為rejected的函數

在下面的代碼中,我們使用setTimeout等待兩秒後觸發resolve。這將會使promise的狀態轉變為fulfilled。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

我們可以輸出記錄來看一下promise兩秒前和兩秒後執行了resolve的狀態變化。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

3)你如何監聽Promise的狀態變化

在我看來,這個是最重要的問題。因為雖然知道如何創建一個promise和改變它的狀態很酷,但是如果我們不知道什麼時候狀態變化該做些什麼,那麼一切將毫無意義。

有一個我沒提到的事情就是promise到底是什麼。當你new一個Promise時,你確實只是創建了一個單純的原始的JavaScript對象。這個對象能夠觸發兩個方法,then和catch。這才是關鍵!當promise的狀態轉變為fulfilled時,then方法會被觸發,當promise的狀態轉變為rejected時,catch方法會被觸發。這意味著你創建了一個promise,同時你要設置當異步請求成功時你想運行的函數到then裡,還有設置當異步請求失敗時應該執行的函數到catch裡。

讓我們來看一個例子。我們會再次使用setTimeout在2秒後改變promise的狀態為fulfilled。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

如果你運行上面的代碼,你會在大概2秒後在控制檯看到“Success!”。原因有二,一是因為我們創建了promise,在大概2000毫秒後觸發了resolve,這轉變了promise的狀態為fulfilled。二是,我們傳遞了onSuccess這個函數給promise的then方法。這樣做的話,我們就相當於告訴promise在大概2秒後狀態轉換為fulfilled時去觸發onSuccess。

現在我們假裝有些不好的情況發生了,我們要去改變promise的狀態為rejected。我們應該調用reject而不是resolve。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

這次,我們調用reject的話,onSuccess函數沒有被觸發,取而代之的是onError函數會被觸發。

現在,你瞭解了圍繞Promise API的方法,讓我們開始看下真實的代碼。

記得我們看過的最後一個異步回調的例子嗎?

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

我們能不能用Promise來改造這裡的回調呢?我們該怎麼把AJAX請求包裹進promise裡?這樣我們就能基於請求的走向來簡單地執行resolve或reject了。讓我們從getUser開始:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

好的,注意getUser的參數已經發生變化了,現在只需接受id就好了。再也不需要另外的那兩個回調函數了,我們也不會再失去控制權。取而代之的是,我們使用Promise的resolve和reject函數。如果請求成功了,resolve會被觸發,如果失敗的話,reject會被觸發。

接下來我們重構getWeather。我們會採用同樣的策略,使用resolve和reject來替換掉onSuccess和onFailure這兩個回調函數:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

看起來很棒。現在最後一件我們需要去更新的是我們的點擊事件。記住,有以下幾點我們需要去做:

  1. 從 GitHub API中取得用戶信息;
  2. 根據用戶位置從Yahoo Weather API獲取天氣信息;
  3. 根據用戶信息和天氣數據刷新UI。

讓我們從第一點開始,從 GitHub API中取得用戶信息:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

注意,現在getUser返回給我們一個promise對象,能用來調用.then和.catch。如果取得了用戶信息的話.then會被執行,如果取得了錯誤信息則.catch會被執行。

接下來來做第二點,根據用戶位置從Yahoo Weather API獲取天氣信息:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

這裡我們採用了跟第一點同樣的手法,並且這裡要傳遞給它從userPromise那裡得到的用戶信息。

最後,根據用戶信息和天氣數據刷新UI:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

咱們的新代碼更好了,但還是有些待改進的問題。在咱們改進之前,有兩個promise的特性你需要知曉,從resolve到then的鏈式調用和參數傳遞。

鏈式調用

.then和.catch都會返回一個promise對象。這看起來是個小細節,但其實這是非常重要的,因為這意味著promise能夠被鏈式調用。

在下面的例子中,我們執行getPromise然後在約2000毫秒後會返回一個promise對象。然後,由於.then也會返回promise對象,所以我們能繼續鏈接.then方法直到拋出錯誤被.catch捕獲到:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

酷!但為什麼這會如此重要呢?回憶一下,回調函數會漸漸衰敗的一個原因就是因為它脫離了你自然、有序的思維方式。當你把promise鏈接在一起,它沒有跳脫你的思維方式,因為這些promise是按順序鏈接的,運行getPromise之後運行then(logA)再運行then(logB)在然後……

再來看一個例子,當你使用fetch API時這裡有一個普遍的用例。fetch會返回給你一個promise用來解析HTTP回應。為了得到實際的JSON數據,你會需要去調用.json,因為鏈式調用,我們能用順序的方式來思考這個過程。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

現在我們瞭解了鏈式調用,讓我們來重構之前的getUser/getWeather代碼:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

參數傳遞

這看起來稍微好點了,但現在我們又面臨了一個問題,你能點出來嗎?在第二個.then裡我們要去調用updateUI。問題是我們需要用戶信息和天氣信息,實際上我們只接收到了天氣信息。我們得設法找出一種方法來讓getWeather返回的promise包含用戶信息和天氣信息。

這裡是關鍵。resolve只是一個函數,任何你傳遞給它的參數也會被傳遞到.then。這不就意味著在getWeather的內部,如果我們觸發了resolve,我們能傳遞給它用戶信息和天氣信息。然後,在第二個.then方法裡我們就能夠接收到這些信息。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

在咱們的點擊事件裡,你能看出promise和callbacks的差距:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

這樣的邏輯看起來會比較自然,因為符合我們通常的思維方式:getUser然後getWeather然後用數據更新UI。


Async/Await

現在,我們清楚地看到了promise迅速地拔高了我們異步代碼的可讀性,但是否還有能讓它更好的方法呢?假設你是TC39委員會的一員,同時你有這個權利去給JavaScript語言增加新的特性。你會採取什麼方法來改善這代碼:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

正如我們所討論的,這代碼讀起來非常好。就如我們的腦子順序化地工作著。但有一個問題是,運行這個代碼我們需要把數據(用戶信息)從第一個異步請求一路往下傳遞到最後一個.then。這不是什麼大毛病,但這讓我們不得不改變getWeather函數來適應傳遞用戶信息數據。如果我們能夠按同步代碼那樣的寫法來寫我們的異步代碼呢?如果能做到的話,這個問題將迎刃而解,同時這樣也保持了代碼的可讀性。這裡有一個主意:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

嗯,這樣看起來很棒。咱們的異步代碼看起來就是同步代碼。我們的腦子可以毫不費力的接受它,因為我們已經非常熟悉這種思考方式了。遺憾的是,這樣顯然是運行不起來的。如你所知,上述代碼中的用戶信息和天氣信息都只是從getUser和getWeather那返回的promise。但是記住,我們是TC39的一員,我們有所有的權力來添加任何我們想要添加的特性到語言上。誠然,要讓這代碼運行起來非常棘手。我們不得不去調試JavaScript引擎來分辨異步代碼和同步代碼。讓我們給代碼添加一些關鍵詞使其更易理解。

首先,讓我們給主函數體上添加一個關鍵字。這能引導引擎知曉在這個函數內部,我們有一些異步函數調用要執行。讓我們使用async這個關鍵字:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

酷。這看起來很合理。接下來讓我們添加一個關鍵字讓引擎知道哪個函數觸發是異步的並且會返回一個promise,讓我們使用await。就像,“嘿,引擎。這個函數是異步的並且會返回一個promise。不要用你那老套的處理法,你繼續向下走同時等待這個promise的最終值,然後在繼續之前返回它就可以了”。藉助新的async和await關鍵字,我們的新代碼看起來像是這樣:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

看起來很妙啊。我們創造了一個讓異步代碼樣子和行為都很像同步代碼的方法。接下來就是該如何去說服TC39的某些人使其知道這是個好主意。幸運的是,你大概猜到了,我們不需要去說服任何人,因為這個特性已經被JavaScript所支持,然後它叫做Async/Await。

async函數返回promise

現在你已經看到了Async/Await的好處了,讓我們討論一些我們需要知道的小細節。首先,只要你給一個函數添加async關鍵字,這個函數必然會返回一個promise。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

雖然getPromise字面上是空函數,但它仍然會返回一個promise。

如果async函數返回一個值,這個值也會被包裹進一個promise裡。這意味著你要用.then去得到它。

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

沒有async的await是不允許的

如果你嘗試在非async函數里使用await關鍵字,你會得到報錯:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

以下是我對此的看法。當你向函數添加async關鍵字時,它會做兩件事。 它使得函數本身返回(或包裹得到的值返回)一個promise,並允許你可以在其中使用await。

錯誤處理

你可能注意到了,在我們原來的代碼裡,我們使用.catch方法來捕獲錯誤信息。但當我們切換為Async/Await時,我們移除了這些代碼。在Async/Await代碼裡,更普遍的做法是將你的代碼包裹進try/catch代碼塊裡來捕獲錯誤:

下篇 JavaScript異步進化史:Callbacks,Promises,Async/Await

轉載自我的百家號(http://baijiahao.baidu.com/builder/preview/s?id=1616286958424121713)


分享到:


相關文章: