JavaScript Promise API

發表於2016-02-25
Promise是抽象非同步處理物件以及對其進行各種操作的元件。
本文將會詳細的向你介紹如何在JavaScript中藉助Promise來簡化非同步程式碼流。

背景知識

JavaScript是單執行緒的,這意味著程式碼是按順序執行的。對於瀏覽器而言,JavaScript程式碼和其他任務共享一個執行緒,
不同的瀏覽器略有差異,但大體上這些和JavaScript共享執行緒的任務主要包括重繪、更新樣式、使用者互動等,
所有這些任務操作都會阻塞其他任務。

避免事件阻塞的常用方法是使用事件監聽器。我們可以為某些特定事件設定監聽器,如果事件發生的話,便立刻觸發監聽器,
你應該已經習慣使用回撥函式來解決這個問題了,例如:

上面的程式碼中,我們新增了兩個監聽器,請求圖片,回撥函式只在事件發生的時候才會被觸發。但是通過事件機制還存在幾個問題:

  1. 事件在繫結之前就發生了怎麼辦?
  2. 在新增監聽器之前,圖片載入發生了錯誤怎麼辦?

僅僅是一張圖片就存在這麼多問題,那麼如果有一堆圖片要處理,又該怎麼辦?下面我們就談談Promise,一個越來越流行的非同步解決方案。

Promise

JavaScript的一大特點就是會涉及到大量的非同步程式碼。同步程式碼通常易於理解和除錯,而非同步程式碼則具有更好的效能和靈活性。
目前Promise正逐漸稱為JavaScript世界的一個重要組成部分,並且很多新的API也都基於Promise進行了實現。
目前已經有一些原生API使用了Promise,包括:

什麼是Promise

那麼到底什麼是Promise呢?Promise是ES6規範新增的物件,它可以用於延遲計算和非同步計算。
一個Promise物件代表著一個還未完成,但預期會完成的操作。需要記住:

  • 一個Promise要麼成功要麼失敗,並且狀態不可變
  • 可以根據Promise的結果設定特定的回撥函式

Promise的狀態

一個Promise的狀態可以是:

  • 等待 pending – Promise的初始化狀態,等待結果
  • 完成 fullfilled – 該Promise對應的非同步操作成功完成了
  • 失敗 rejected – 該Promise對應的非同步操作失敗了
  • 結束 settled – 任務完成或失敗了

基本使用

new Promise()構造器應該只被用於傳統的非同步任務上,例如setTimeoutXMLHttpRequest
通過new關鍵字建立一個新的Promise,它接收一個回撥函式作為引數,該回撥函式又包括了兩個特定的回撥函式,
分別被命名為resolvereject,成功後呼叫resolve,失敗則呼叫reject

根據不同的任務,由開發者來決定resolvereject在函式體內的位置。

使用Promise則非常的簡單,可以呼叫Promise物件的then()方法來處理非同步計算的結果。then接收兩個回撥函式,
分別是成功的回撥函式和失敗時的回撥函式,這兩個引數都是可選的。

Promise的使用有兩點需要記住的:

  1. then()方法可以鏈式呼叫
  2. catch()方法可以作為錯誤處理語句的語法糖,相當於then(undefined, function(error) { ... });

在具體講解這兩點之前,我們先來看一個例子。下面這個例子用於將XMLHttpRequest轉換為一個基於Promise的介面。
我們以GET請求為例:

我們現在可以這麼呼叫它:

現在我們發起XHR請求便變得簡單直觀的多了。story.json檔案的內容如下:

Promise.resolve

有時你無需在promise內完成一個非同步任務——如果一個非同步動作被執行是可能的話,然而,返回一個Promise是將是最合適的,
因此你可以總是期望從給定函式中產生的promise。在這種情況下,你可以簡單的呼叫Promise.resolve()或者Promise.reject()
而無需new關鍵字。例如:

由於返回的是一個Promise,你可以在返回值上使用thencatch方法。
可以將Promise.resolve看作是new Promise()的快捷方式。

鏈式呼叫

上面我們說過then()接收兩個引數,分別對應成功和失敗時的回撥函式。我們還可以將多個then方法串聯起來,
用於修改結果或執行更多的非同步操作。

你可以對結果進行修改,然後返回一個新的值,例如:

每個then接收前一個then的返回值的結果。

回到之前的get函式,我們可以修改返回值的型別,將結果進行一定的轉換:

為了讓程式碼變得更簡單,可以再次進行改進:

  • 因為JSON.parse只接收一個引數,並返回轉換後的結果,我們可以直接使用then(JSON.parse)
  • then中的回撥函式,我們可以直接使用ES6的胖箭頭函式,這樣可以讓程式碼更直觀

由於這段程式碼會被重複呼叫,我們可以定義一個新的getJSON函式:

對於串聯起來的then()方法而言:如果你返回了一個值,那麼它就會被傳給下一個then()的回撥。
如果你返回一個“類Promise”物件,則下一個then()就會等待這個Promise明確結束(成功/失敗)才會執行。

在上面的程式碼中,我們首先發起對story.json的非同步請求,它會返回給我們一個URL列表,然後我們請求其中的第一個,。

錯誤處理

前面我們已經知道,then接收兩個引數,一個處理成功時的回撥函式,一個處理失敗時的回撥函式。

你還可以使用catch來進行錯誤處理,實際上,它不過是then(undefined, func)的語法糖而已。這樣能夠讓程式碼更直觀:

並行和序列

非同步意味著你不用等待前一件事情做完就可以做後一件事。現在,我們想要遍歷所有章節的URL並且依次請求,應該怎麼辦?
使用傳統的方法,你可能會想到array.forEach

但是這麼做並不可行,因為forEach不支援非同步操作

Promise序列

上面的程式碼中使用了Promise.resolve(),它會依據你傳入的任何值返回一個Promise。
如果你傳給它一個類Promise物件(帶有then方法),它會生成一個帶有同樣肯定/否定回撥的Promsie。
如果你傳給它任何別的值,如Promise.resolve('hello'),它會建立一個以該值為完成結果的Promise
如過不傳入任何值,則以undefined為完成結果。

reduce回撥會一次應用在每一個陣列元素上,第一輪的sequencePromise.resolve()
之後的呼叫裡sequence就是上次函式執行的結果。reduce()方法非常適合用於把一個值歸併處理為一個值。

Array.prototype.reduce(callback, [initialValue])方法接收一個函式作為累加器,陣列中的每個值(從左到右)開始合併,
最終為一個值。引數二作為第一次呼叫callback的第一個引數。此外,callback包括四個引數:

  • previousValue – 上一次呼叫回撥返回的值,或者是提供的初始值(initialValue)
  • currentValue – 陣列中當前被處理的元素
  • index – 當前元素在陣列中的索引
  • array – 呼叫reduce的陣列

彙總前面的程式碼為:

輔助方法定義如下:

Promise.all

瀏覽器很擅長同時載入多個檔案,上面的方法屬於一個接一個下載章節,這先得非常的低效。我們希望同時下載所有章節,
全部完成後一次搞定,正好就有這麼個API:

Promise.all接收一個Promise陣列作為引數,建立一個當所有Promise都完成之後就完成的Promise,它的完成結果是一個陣列,
包含了所有先前傳入的那些Promise的完成結果,順序和將它們傳入的陣列順序一致。

根據連線狀況,改進的程式碼會比順序載入方式提速數秒,甚至程式碼行數也更少。章節載入完成的順序不確定,
但它們顯示在頁面上的順序準確無誤。

但仍然有改進空間:第一章內容載入完成後,我們向讓它立即填進頁面,這樣使用者可以在其他載入任務尚未完成之前就開始閱讀。
當第三章到達的時候我們不動聲色,第二章也到達之後我們再把第二章和第三章內容填入頁面,以此類推。

為了達到這個效果,我們同時請求所有的章節內容,然後建立一個序列依次將其填入頁面:

References

  1. JavaScript Promise API
  2. JavaScript Promise: There and Back Again
  3. Promise迷你書
  4. MDN: Promise

相關文章