本文主要參考了JavaScript Promise迷你書,連接在文末與其餘參考一塊兒列出。html
Promise是異步編程的一種解決方案。ES6 Promise的規範來源於Promises/A+社區,它有不少版本的實現。git
Promise比傳統的解決方案(回調函數和事件)更合理和更強大,能夠避免回調地獄。使用Promise來統一處理異步操做,更具語義化、易於理解、有利維護。es6
Promise接口的基本思想是讓異步操做返回一個Promise對象,咱們能夠對這個對象進行一些操做。github
Promise對象只有三種狀態。web
這三種的狀態的變化途徑只有兩種。ajax
這種變化只能發生一次,一旦當前狀態變爲「已完成」或「失敗」,就意味着不會再有新的狀態變化了。所以,Promise對象的最終結果只有兩種。chrome
異步操做成功,Promise對象傳回一個值,狀態變爲resolved。npm
異步操做失敗,Promise對象拋出一個錯誤,狀態變爲rejected。編程
目前主要有三種類型json
1) 構造函數(Constructor)
建立一個promise實例:
var promise = new Promise(function (resolve, reject) { // 異步處理 // 處理結束後、調用resolve 或 reject })
2) 實例方法(Instance Method)
promise.then(onFulfilled, onRejected) promise.catch(onRejected)
3) 靜態方法(Static Method)
Promise.all()、 Promise.race()、Promise.resolve()、Promise.reject()
給Promise構造函數傳遞一個函數fn做爲參數實例化便可。這個函數fn有兩個參數(resolve和reject),在fn中指定異步等處理:
// 建立promise對象基本形式 var promise = new Promise(function (resolve, reject) { // ... some code if (/* 異步操做成功 */) { resolve(value) } else { reject(error) } }) // 將圖片加載轉爲promise形式 var preloadImage = function (path) { return new Promise(function (resolve, reject) { var image = new Image() image.onload = resolve image.onerror = reject image.src = path }) } // 建立XHR的promise對象 function getURL (URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest() req.open('GET', URL, true) req.onload = function () { if (req.status === 200) { resolve(req.responseText) } else { reject(new Error(req.statusText)) } } req.onerror = function () { reject(new Error(req.statusText)) } req.send() }) } // 運行示例 var URL = 'http://httpbin.org/get' getURL(URL) .then(function onFulfilled (value){ console.log(value) }) .catch(function onRejected (error){ console.error(error) })
getURL只有在經過XHR取得結果狀態爲200時纔會調用resolve。也就是隻有數據取得成功時,而其餘狀況(取得失敗)時則會調用reject方法。
resolve(req.responseText)在response的內容中加入了參數。resolve方法的參數並無特別的規則,基本上把要傳給回調函數參數放進去就能夠了。(then方法能夠接收到這個參數值)
爲promise對象添加處理方法主要有如下兩種:
被resolve後的處理,能夠在.then方法中傳入想要調用的函數:
var URL = 'http://httpbin.org/get' getURL(URL).then(function onResolved(value){ console.log(value) })
被reject後的處理,能夠在.then的第二個參數或者是在.catch方法中設置想要調用的函數。
var URL = 'http://httpbin.org/status/500' getURL(URL) .then(function onResolved(value){ console.log(value) }) .catch(function onRejected(error){ console.error(error) })
.catch只是promise.then(undefined, onRejected)的別名而已,以下代碼也能夠完成一樣的功能。
getURL(URL).then(onResolved, onRejected)
1)new Promise的快捷方式
靜態方法Promise.resolve(value)能夠認爲是new Promise()方法的快捷方式。Promise.resolve(value)返回一個狀態由給定value決定的Promise對象。若是該值是一個Promise對象,則直接返回該對象;若是該值是thenable對象(見下面部分2),返回的Promise對象的最終狀態由then方法執行決定;不然的話(該value爲空,基本類型或者不帶then方法的對象),返回的Promise對象狀態爲resolved,而且將該value傳遞給對應的then方法。
因此和new Promise()方法並不徹底一致。Promise.resolve接收一個promise對象會直接返回這個對象。而new Promise()老是新生成一個promise對象。
var p1 = Promise.resolve(1) var p2 = Promise.resolve(p1) var p3 = new Promise(function (resolve, reject) { resolve(p1) }) console.log(p1 === p2) // true console.log(p1 === p3) // false
經常使用Promise.resolve()快速初始化一個promise對象。
Promise.resolve(42).then(function (value) { console.log(value) })
2)Promise.resolve方法另外一個做用就是將thenable對象轉換爲promise對象。
什麼是thenable對象?Thenable對象能夠認爲是類Promise對象,擁有名爲.then方法的對象。和類數組的概念類似。
有哪些thenable對象?主要是ES6以前有許多庫實現了Promise,其中有不少與ES6 Promise規範並不一致,咱們稱這些與ES6中的promise對象相似而又有差別的對象爲thenable對象。如jQuery中的ajax()方法返回的對象。
// 將thenable對象轉換promise對象 var promise = Promise.resolve($.ajax('/json/comment.json')) // => promise對象 promise.then(function (value) { console.log(value) })
Promise.reject(error)是和Promise.resolve(value)相似的靜態方法,是new Promise()方法的快捷方式。
好比Promise.reject(new Error('出錯了'))就是下面代碼的語法糖形式:
new Promise(function (resolve, reject) { reject(new Error('出錯了')) })
Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1, p2, p3])
上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是Promise實例,若是不是,就會先調用Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。
p的狀態由p一、p二、p3決定,分紅兩種狀況。
(1)只有p一、p二、p3的狀態都變成resolved,p的狀態纔會變成resolved,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
傳遞給Promise.all的promise並非一個個的順序執行的,而是同時開始、並行執行的。
// `delay`毫秒後執行resolve function timerPromisefy (delay) { return new Promise(function (resolve) { setTimeout(function () { resolve(delay) }, delay) }) } var startDate = Date.now() // 全部promise變爲resolve後程序退出 Promise.all([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ]).then(function (values) { console.log(Date.now() - startDate + 'ms') // 約128ms console.log(values) // [1, 32, 64, 128] })
從上述結果能夠看出,傳遞給Promise.all的promise並非一個個的順序執行的,而是同時開始、並行執行的。
若是這些promise所有串行處理的話,那麼須要等待1ms → 等待32ms → 等待64ms → 等待128ms ,所有執行完畢須要約225ms的時間。
var p = Promise.race([p1, p2, p3])
與Promise.all相似,可是隻要p一、p二、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的Promise實例的返回值,就傳遞給p的回調函數。
// `delay`毫秒後執行resolve function timerPromisefy(delay) { return new Promise(function (resolve) { setTimeout(function () { resolve(delay) }, delay) }) } // 任何一個promise變爲resolve或reject的話程序就中止運行 Promise.race([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64), timerPromisefy(128) ]).then(function (value) { console.log(value) // => 1 })
下面咱們再來看看在第一個promise對象變爲肯定(resolved)狀態後,它以後的promise對象是否還在繼續運行:
var winnerPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is winner') resolve('this is winner') }, 4) }) var loserPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is loser') resolve('this is loser') }, 1000) }) // 第一個promise變爲resolve後程序中止 Promise.race([winnerPromise, loserPromise]).then(function (value) { console.log(value) // => 'this is winner' })
執行上面代碼的話,咱們會看到winnter和loser promise對象的setTimeout方法都會執行完畢,console.log也會分別輸出它們的信息。
也就是說,Promise.race在第一個promise對象變爲Fulfilled以後,並不會取消其餘promise對象的執行。
在ES6 Promises規範中,也沒有取消(中斷)promise對象執行的概念,咱們必需要確保promise最終進入resolve or reject狀態之一。也就是說Promise並不適用於狀態可能會固定不變的處理。也有一些類庫提供了對promise進行取消的操做。
因爲不少瀏覽器不支持ES6 Promises,咱們須要一些第三方實現的和Promise兼容的類庫。
選擇Promise類庫首先要考慮的是否具備Promises/A+兼容性。
Promises/A+是ES6 Promises的前身,Promise的then也是由社區的規範而來。
這些類庫主要有兩種:Polyfill和擴展類庫
1)Polyfill
2)Promise擴展類庫
Q等文檔裏詳細介紹了Q的Deferred和jQuery裏的Deferred有哪些異同,以及要怎麼進行遷移等都進行了詳細的說明。
1)done()
Promise對象的回調鏈,無論以then方法或catch方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到(由於Promise內部的錯誤不會冒泡到全局)。所以,咱們能夠提供一個done方法,老是處於回調鏈的尾端,保證拋出任何可能出現的錯誤。
'use strict' if (typeof Promise.prototype.done === 'undefined') { Promise.prototype.done = function (onFulfilled, onRejected) { this.then(onFulfilled, onRejected) .catch(function (error) { setTimeout(function () { throw error }, 0) }) } } // 調用 asyncFunc() .then(f1) .catch(r1) .then(f2) .done()
從上面代碼能夠看到done有如下兩個特色。
那麼它是如何將異常拋到Promise的外面的呢?其實這裏咱們利用的是在setTimeout中使用throw方法,直接將異常拋給了外部。
// setTimeout的回調函數中拋出異常 try { setTimeout(function callback () { throw new Error('error') }, 0) } catch (error) { console.error(error) }
由於異步的callback中拋出的異常不會被捕獲,上面例子中的例外不會被捕獲。
ES6 Promises和Promises/A+等在設計上並無對Promise.prototype.done作出任何規定,可是爲何不少類庫都提供了該方法的實現呢?
主要是防止編碼時忘記使用catch方法處理異常致使錯誤排查很是困難的問題。因爲Promise的try-catch機制,異常可能會被內部消化掉。這種錯誤被內部消化的問題也被稱爲unhandled rejection,從字面上看就是在Rejected時沒有找到相應處理的意思。
function JSONPromise (value) { return new Promise(function (resolve) { resolve(JSON.parse(value)) }) } // 運行示例 var string = '{}' JSONPromise(string).then(function (object) { conosle.log(object) })
在這個例子裏,咱們錯把console拼成了conosle,所以會發生以下錯誤:
ReferenceError: conosle is not defined
不過在chrome中實測查找這種錯誤已經至關精準了。因此之前用jQuery的時候用過done,後來在實際項目中並無使用過done方法。
2)finally()
finally方法用於指定無論Promise對象最後狀態如何,都會執行的操做。它與done方法的最大區別,它接受一個普通的回調函數做爲參數,該函數無論怎樣都必須執行。
Promise.prototype.finally = function (callback) { let P = this.constructor return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ) }
這個仍是頗有用的,咱們常常在ajax不管成功仍是失敗後都要關閉loading。我通常使用這個庫promise.prototype.finally。
var promise = new Promise(function (resolve) { console.log(1) // 1 resolve(3) }) promise.then(function(value){ console.log(value) // 3 }) console.log(2) // 2
執行上面的代碼,會依次輸出1,2,3。首先new Promise中的函數會當即執行,而後是外面的console.log(2),最後是then回調中的函數。
因爲promise.then執行的時候promise對象已是肯定狀態,從程序上說對回調函數進行同步調用也是行得通的。
可是即便在調用promise.then註冊回調函數的時候promise對象已是肯定的狀態,Promise也會以異步的方式調用該回調函數,這是在Promise設計上的規定方針。爲何要這樣呢?
這涉及到同步調用和異步調用同時存在致使的混亂。
function onReady (fn) { var readyState = document.readyState if (readyState === 'interactive' || readyState === 'complete') { fn() } else { window.addEventListener('DOMContentLoaded', fn) } } onReady(function () { console.log('DOM fully loaded and parsed') }) console.log('==Starting==')
上面的代碼若是在調用onReady以前DOM已經載入的話:對回調函數進行同步調用。
若是在調用onReady以前DOM尚未載入的話:經過註冊DOMContentLoaded事件監聽器來對回調函數進行異步調用。
所以,若是這段代碼在源文件中出現的位置不一樣,在控制檯上打印的log消息順序也會不一樣。
爲了解決這個問題,咱們能夠選擇統一使用異步調用的方式:
function onReady (fn) { var readyState = document.readyState if (readyState === 'interactive' || readyState === 'complete') { setTimeout(fn, 0) } else { window.addEventListener('DOMContentLoaded', fn) } } onReady(function () { console.log('DOM fully loaded and parsed') }) console.log('==Starting==')
關於這個問題,在Effective JavaScript的第67項不要對異步回調函數進行同步調用中也有詳細介紹:
爲了不上述中同時使用同步、異步調用可能引發的混亂問題,Promise在規範上規定Promise只能使用異步調用方式。
因爲Promise保證了每次調用都是以異步方式進行的,因此咱們在實際編碼中不須要調用setTimeout來本身實現異步調用:
function onReadyPromise () { return new Promise(function (resolve, reject) { var readyState = document.readyState if (readyState === 'interactive' || readyState === 'complete') { resolve() } else { window.addEventListener('DOMContentLoaded', resolve) } }) } onReadyPromise().then(function () { console.log('DOM fully loaded and parsed') }) console.log('==Starting==')
前面Promise.resolve()章節的三個promise,咱們看看其執行順序是怎樣的?
var p1 = Promise.resolve(1) var p2 = Promise.resolve(p1) var p3 = new Promise(function (resolve, reject) { resolve(p1) }) var p4 = new Promise(function (resolve, reject) { reject(p1) }) p3.then(function (value) { console.log('p3 : ' + value) }) p2.then(function (value) { console.log('p2 : ' + value) }) p4.then(function (value) { console.log('p4-1 : ' + value) }, function (value) { console.log('p4-1 : ' + value) }) p4.then(function (value) { console.log('p4-2 : ' + value) }).catch(function (value) { console.log('p4-2 : ' + value) }) p1.then(function (value) { console.log('p1 : ' + value) })
咱們在比較新的瀏覽器控制檯輸出會發現順序爲2,4-1,1,4-2,3(測試發現chrome5五、56中則是最早打印出3)。這個不知道怎麼解釋了,爲何p3會最後執行?暫時沒找到什麼可靠的資料,有大神知道的話,請評論指出。
function taskA () { console.log('Task A') } function taskB () { console.log('Task B') } function onRejected (error) { console.log('Catch Error: A or B', error) } function finalTask () { console.log('Final Task') } var promise = Promise.resolve() promise .then(taskA) .then(taskB) .catch(onRejected) .then(finalTask)
在上述代碼中,咱們沒有爲then方法指定第二個參數(onRejected),能夠像下面這樣來理解:
then:註冊onResolved時的回調函數
catch:註冊onRejected時的回調函數
1)taskA、taskB都沒有發生異常,會按照taskA → taskB → finalTask這個流程來進行處理
2)taskA沒有發生異常,taskB發生異常,會按照taskA → taskB → onRejected → finalTask這個流程來進行處理
3)taskA發生異常,會按照taskA → onRejected → finalTask這個流程來進行處理,TaskB是不會被調用的
function taskA () { console.log('Task A') throw new Error('throw Error @ Task A') } function taskB () { console.log('Task B') // 不會被調用 } function onRejected (error) { console.log(error) // => 'throw Error @ Task A' } function finalTask () { console.log('Final Task') } var promise = Promise.resolve() promise .then(taskA) .then(taskB) .catch(onRejected) .then(finalTask)
在本例中咱們在taskA中使用了throw方法故意製造了一個異常。但在實際中想主動進行onRejected調用的時候,應該返回一個Rejected狀態的promise對象。
若是Task A想給Task B傳遞一個參數該怎麼辦呢?其實很是簡單,只要在taskA中return一個值,這個值會做爲參數傳遞給taskB。
function doubleUp (value) { return value * 2 } function increment (value) { return value + 1 } function output (value) { console.log(value) // => (1 + 1) * 2 } var promise = Promise.resolve(1) promise .then(increment) .then(doubleUp) .then(output) .catch(function (error) { // promise chain中出現異常的時候會被調用 console.error(error) })
每一個方法中return的值不只只侷限於字符串或者數值類型,也能夠是對象或者promise對象等複雜類型。
return的值會由Promise.resolve(return的返回值)進行相應的包裝處理,所以無論回調函數中會返回一個什麼樣的值,最終then的結果都是返回一個新建立的promise對象。
也就是說,Promise的then方法不只僅是註冊一個回調函數那麼簡單,它還會將回調函數的返回值進行變換,建立並返回一個promise對象。
在使用Promise處理一些複雜邏輯的過程當中,咱們有時候會想要在發生某種錯誤後就中止執行Promise鏈後面全部的代碼。
然而Promise自己並無提供這樣的功能,一個操做,要麼成功,要麼失敗,要麼跳轉到then裏,要麼跳轉到catch裏。
具體怎麼作,請查看這篇文章從如何停掉 Promise 鏈提及。
從代碼上乍一看,aPromise.then(...).catch(...)像是針對最初的aPromise對象進行了一連串的方法鏈調用。
然而實際上無論是then仍是catch方法調用,都返回了一個新的promise對象。
var aPromise = new Promise(function (resolve) { resolve(100) }) var thenPromise = aPromise.then(function (value) { console.log(value) }) var catchPromise = thenPromise.catch(function (error) { console.error(error) }) console.log(aPromise !== thenPromise) // => true console.log(thenPromise !== catchPromise) // => true
執行上面代碼,證實了then和catch都返回了和調用者不一樣的promise對象。知道了這點,咱們就很容易明白下面兩種調用方法的區別:
// 1: 對同一個promise對象同時調用 `then` 方法 var aPromise = new Promise(function (resolve) { resolve(100) }) aPromise.then(function (value) { return value * 2 }) aPromise.then(function (value) { return value * 2 }) aPromise.then(function (value) { console.log('1: ' + value) // => 100 }) // vs // 2: 對 `then` 進行 promise chain 方式進行調用 var bPromise = new Promise(function (resolve) { resolve(100) }) bPromise.then(function (value) { return value * 2 }).then(function (value) { return value * 2 }).then(function (value) { console.log('2: '' + value) // => 100 * 2 * 2 })
下面是一個由方法1中的then用法致使的比較容易出現的頗有表明性的反模式的例子:
// then的錯誤使用方法 function badAsyncCall() { var promise = Promise.resolve() promise.then(function() { // 任意處理 return newVar }) return promise }
這種寫法有不少問題,首先在promise.then中產生的異常不會被外部捕獲,此外,也不能獲得then的返回值,即便其有返回值。
不只then和catch都返回了和調用者不一樣的promise對象,Promise.all和Promise.race,他們都會接收一組promise對象爲參數,並返回一個和接收參數不一樣的、新的promise對象。
以前咱們說過 .catch也能夠理解爲promise.then(undefined, onRejected)。那麼使用這兩種方法進行錯誤處理有什麼區別呢?
function throwError (value) { // 拋出異常 throw new Error(value) } // <1> onRejected不會被調用 function badMain (onRejected) { return Promise.resolve(42).then(throwError, onRejected) } // <2> 有異常發生時onRejected會被調用 function goodMain (onRejected) { return Promise.resolve(42).then(throwError).catch(onRejected) } // 運行示例 badMain(function () { console.log("BAD") }) goodMain(function () { console.log("GOOD") })
在上面的代碼中,badMain是一個不太好的實現方式(但也不是說它有多壞),goodMain則是一個能很是好的進行錯誤處理的版本。
爲何說badMain很差呢?,由於雖然咱們在.then的第二個參數中指定了用來錯誤處理的函數,但實際上它卻不能捕獲第一個參數onResolved指定的函數(本例爲 throwError)裏面出現的錯誤。
也就是說,這時候即便throwError拋出了異常,onRejected指定的函數也不會被調用(即不會輸出"BAD"字樣)。
與此相對的是,goodMain的代碼則遵循了throwError → onRejected的調用流程。這時候throwError中出現異常的話,在會被方法鏈中的下一個方法,即.catch所捕獲,進行相應的錯誤處理。
.then方法中的onRejected參數所指定的回調函數,實際上針對的是其promise對象或者以前的promise對象,而不是針對.then方法裏面指定的第一個參數,即onResolved所指向的對象,這也是then和catch表現不一樣的緣由。
1)使用promise.then(onResolved, onRejected)的話
在onResolved中發生異常的話,在onRejected中是捕獲不到這個異常的。
2)在promise.then(onResolved).catch(onRejected)的狀況下
then中產生的異常能在.catch中捕獲
3).then和.catch在本質上是沒有區別的
須要分場合使用。
咱們須要注意若是代碼相似badMain那樣的話,就可能出現程序不會按預期運行的狀況,從而不能正確的進行錯誤處理。
IE8及IE8如下即便已經引入了Promise的polyfill,使用catch方法仍然會出現identifier not found的語法錯誤。
這是怎麼回事呢?實際上這和catch是ECMAScript的保留字(Reserved Word)有關。
在ECMAScript 3中保留字是不能做爲對象的屬性名使用的。而IE8及如下版本都是基於ECMAScript 3實現的,所以不能將catch做爲屬性來使用,也就不能編寫相似promise.catch()的代碼,所以就出現了identifier not found這種語法錯誤了。
而現代瀏覽器都支持ECMAScript 5,而在ECMAScript 5中保留字都屬於IdentifierName,也能夠做爲屬性名使用了。
點標記法(dot notation)要求對象的屬性必須是有效的標識符(在ECMAScript 3中則不能使用保留字)。
可是使用中括號標記法(bracket notation)的話,則能夠將非合法標識符做爲對象的屬性名使用。
var promise = Promise.reject(new Error('message')) promise['catch'](function (error) { console.error(error) })
因爲catch標識符可能會致使問題出現,所以一些類庫(Library)也採用了caught做爲函數名,而函數要完成的工做是同樣的。
並且不少壓縮工具自帶了將promise.catch轉換爲promise['catch']的功能,因此可能不經意之間也能幫咱們解決這個問題。
var promise = new Promise(function (resolve, reject) { throw new Error("message") }) promise.catch(function (error) { console.error(error) // => "message" })
上面代碼其實並無什麼問題,可是有兩個很差的地方:
首先是由於咱們很難區分throw是咱們主動拋出來的,仍是由於真正的其它異常致使的。
其次原本這是和調試沒有關係的地方,throw時就會觸發調試器的break行爲,會干擾瀏覽器的調試器中break的功能的正常使用。
因此使用reject會比使用throw安全。
以前咱們已經講過Promise.resolve能將thenable對象轉化爲promise對象。接下來咱們再看看將thenable對象轉換爲promise對象這個功能都能具體作些什麼事情。
以Web Notification爲例,普通使用回調函數方式以下:
function notifyMessage (message, options, callback) { if (Notification && Notification.permission === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else if (Notification.requestPermission) { Notification.requestPermission(function (status) { if (Notification.permission !== status) { Notification.permission = status } if (status === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else { callback(new Error('user denied')) } }) } else { callback(new Error('doesn\'t support Notification API')) } } // 運行實例 // 第二個參數是傳給 `Notification` 的option對象 notifyMessage('Hi!', {}, function (error, notification) { if (error) { return console.error(error) } console.log(notification) // 通知對象 })
使用Promise改寫回調:
function notifyMessage (message, options, callback) { if (Notification && Notification.permission === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else if (Notification.requestPermission) { Notification.requestPermission(function (status) { if (Notification.permission !== status) { Notification.permission = status } if (status === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else { callback(new Error('user denied')) } }) } else { callback(new Error('doesn\'t support Notification API')) } } function notifyMessageAsPromise (message, options) { return new Promise(function (resolve, reject) { notifyMessage(message, options, function (error, notification) { if (error) { reject(error) } else { resolve(notification) } }) }) } // 運行示例 notifyMessageAsPromise('Hi!').then(function (notification) { console.log(notification) // 通知對象 }).catch(function(error){ console.error(error) })
使用thenable對象形式:
function notifyMessage (message, options, callback) { if (Notification && Notification.permission === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else if (Notification.requestPermission) { Notification.requestPermission(function (status) { if (Notification.permission !== status) { Notification.permission = status } if (status === 'granted') { var notification = new Notification(message, options) callback(null, notification) } else { callback(new Error('user denied')) } }) } else { callback(new Error('doesn\'t support Notification API')) } } // 返回 `thenable` function notifyMessageAsThenable (message, options) { return { 'then': function (resolve, reject) { notifyMessage(message, options, function (error, notification) { if (error) { reject(error) } else { resolve(notification) } }) } } } // 運行示例 Promise.resolve(notifyMessageAsThenable('message')).then(function (notification) { console.log(notification) // 通知對象 }).catch(function (error) { console.error(error) })
Thenable風格表現爲位於回調和Promise風格中間的一種狀態,不用考慮Promise的兼容問題。通常不做爲類庫的公開API,更多狀況下是在內部使用Thenable。Thenable對象更多的是用來在Promise類庫之間進行相互轉換。
使用thenable將promise對象轉換爲Q promise對象:
var Q = require('Q') // 這是一個ES6的promise對象 var promise = new Promise(function (resolve) { resolve(1) }) // 變換爲Q promise對象 Q(promise).then(function (value) { console.log(value) }).finally(function () { // Q promise對象可使用finally方法 console.log('finally') })
Deferred和Promise不一樣,它沒有共通的規範,每一個Library都是根據本身的喜愛來實現的。
在這裏,咱們打算以jQuery.Deferred相似的實現爲中心進行介紹。
簡單來講,Deferred和Promise具備以下的關係。
用Deferred實現的getURL(Deferred基於promise實現):
function Deferred () { this.promise = new Promise(function (resolve, reject) { this._resolve = resolve this._reject = reject }.bind(this)) } Deferred.prototype.resolve = function (value) { this._resolve.call(this.promise, value) } Deferred.prototype.reject = function (reason) { this._reject.call(this.promise, reason) } function getURL (URL) { var deferred = new Deferred() var req = new XMLHttpRequest() req.open('GET', URL, true) req.onload = function () { if (req.status === 200) { deferred.resolve(req.responseText) } else { deferred.reject(new Error(req.statusText)) } } req.onerror = function () { deferred.reject(new Error(req.statusText)) } req.send() return deferred.promise } // 運行示例 var URL = 'http://httpbin.org/get' getURL(URL).then(function onFulfilled (value){ console.log(value) }).catch(console.error.bind(console))
Promise實現的getURL:
function getURL (URL) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest() req.open('GET', URL, true) req.onload = function () { if (req.status === 200) { resolve(req.responseText) } else { reject(new Error(req.statusText)) } } req.onerror = function () { reject(new Error(req.statusText)) } req.send() }) } // 運行示例 var URL = 'http://httpbin.org/get' getURL(URL).then(function onFulfilled (value){ console.log(value) }).catch(console.error.bind(console))
對比上述兩個版本的getURL ,咱們發現它們有以下不一樣。
在如下方面,它們則完成了一樣的工做。
因爲Deferred包含了Promise,因此大致的流程仍是差很少的,不過Deferred有對Promise進行操做的特權方法,以及能夠對流程控制進行自由定製。
上面咱們只是簡單的實現了一個Deferred ,我想你已經看到了它和Promise之間的差別了吧。
若是說Promise是用來對值進行抽象的話,Deferred則是對處理尚未結束的狀態或操做進行抽象化的對象,咱們也能夠從這一層的區別來理解一下這二者之間的差別。
換句話說,Promise表明了一個對象,這個對象的狀態如今還不肯定,可是將來一個時間點它的狀態要麼變爲正常值(FulFilled),要麼變爲異常值(Rejected);而Deferred對象表示了一個處理尚未結束的這種事實,在它的處理結束的時候,能夠經過Promise來取得處理結果。
XHR有一個timeout屬性,使用該屬性也能夠簡單實現超時功能,可是爲了能支持多個XHR同時超時或者其餘功能,咱們採用了容易理解的異步方式在XHR中經過超時來實現取消正在進行中的操做。
1)讓Promise等待指定時間
function delayPromise (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms) }) } delayPromise(100).then(function () { alert('已通過了100ms!') })
2) 使用promise.race()來實現超時promise:
function timeoutPromise (promise, ms) { var timeout = delayPromise(ms).then(function () { throw new Error('Operation timed out after ' + ms + ' ms') }) return Promise.race([promise, timeout]) }
上面代碼promise的狀態改變的時間超過了ms就會throw Error。
// 運行示例 var taskPromise = new Promise(function(resolve){ // 隨便一些什麼處理 var delay = Math.random() * 2000 setTimeout(function() { resolve(delay + 'ms') }, delay) }) timeoutPromise(taskPromise, 1000).then(function (value) { console.log('taskPromise在規定時間內結束 : ' + value) }).catch(function (error) { console.log('發生超時', error) })
3)定製Error對象
爲了能區分這個Error對象的類型,咱們再來定義一個Error對象的子類TimeoutError。
function copyOwnFrom (target, source) { Object.getOwnPropertyNames(source).forEach(function (propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)) }) return target } function TimeoutError () { var superInstance = Error.apply(null, arguments) copyOwnFrom(this, superInstance) } TimeoutError.prototype = Object.create(Error.prototype) TimeoutError.prototype.constructor = TimeoutError
它的使用方法和普通的Error對象同樣,使用throw語句便可
var promise = new Promise(function () { throw new TimeoutError('timeout') }) promise.catch(function (error) { console.log(error instanceof TimeoutError) // true })
有了這個TimeoutError對象,咱們就能很容易區分捕獲的究竟是由於超時而致使的錯誤,仍是其餘緣由致使的Error對象了。
4)經過超時取消XHR操做
取消XHR操做自己的話並不難,只須要調用XMLHttpRequest對象的abort()方法就能夠了。
爲了能在外部調用abort()方法,咱們先對以前本節出現的getURL進行簡單的擴展,cancelableXHR方法除了返回一個包裝了XHR的promise對象以外,還返回了一個用於取消該XHR請求的abort方法。
function copyOwnFrom (target, source) { Object.getOwnPropertyNames(source).forEach(function (propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)) }) return target } function TimeoutError () { var superInstance = Error.apply(null, arguments) copyOwnFrom(this, superInstance) } TimeoutError.prototype = Object.create(Error.prototype) TimeoutError.prototype.constructor = TimeoutError function delayPromise (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms) }) } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function () { return Promise.reject(new TimeoutError('Operation timed out after ' + ms + ' ms')) }) return Promise.race([promise, timeout]) } function cancelableXHR(URL) { var req = new XMLHttpRequest() var promise = new Promise(function (resolve, reject) { req.open('GET', URL, true) req.onload = function () { if (req.status === 200) { resolve(req.responseText) } else { reject(new Error(req.statusText)) } } req.onerror = function () { reject(new Error(req.statusText)) } req.onabort = function () { reject(new Error('abort this request')) } req.send() }) var abort = function () { // 若是request尚未結束的話就執行abort // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest if (req.readyState !== XMLHttpRequest.UNSENT) { req.abort() } } return { promise: promise, abort: abort } } var object = cancelableXHR('http://httpbin.org/get') // main timeoutPromise(object.promise, 1000).then(function (contents) { console.log('Contents', contents) }).catch(function (error) { if (error instanceof TimeoutError) { object.abort() return console.log(error) } console.log('XHR Error :', error) })
5)代碼分割優化處理
在前面的cancelableXHR中,promise對象及其操做方法都是在一個對象中返回的,看起來稍微有些不太好理解。
從代碼組織的角度來講一個函數只返回一個值(promise對象)是一個很是好的習慣,可是因爲在外面不能訪問cancelableXHR方法中建立的req變量,因此咱們須要編寫一個專門的函數(上面的例子中的abort)來對這些內部對象進行處理。
固然也能夠考慮到對返回的promise對象進行擴展,使其支持abort方法,可是因爲promise對象是對值進行抽象化的對象,若是不加限制的增長操做用的方法的話,會使總體變得很是複雜。
你們都知道一個函數作太多的工做都不認爲是一個好的習慣,所以咱們不會讓一個函數完成全部功能,也許像下面這樣對函數進行分割是一個不錯的選擇。
將這些處理整理爲一個模塊的話,之後擴展起來也方便,一個函數所作的工做也會比較精煉,代碼也會更容易閱讀和維護。
使用common.js規範來寫cancelableXHR.js:
'use strict' var requestMap = {} function createXHRPromise (URL) { var req = new XMLHttpRequest() var promise = new Promise(function (resolve, reject) { req.open('GET', URL, true) req.onreadystatechange = function () { if (req.readyState === XMLHttpRequest.DONE) { delete requestMap[URL] } } req.onload = function () { if (req.status === 200) { resolve(req.responseText) } else { reject(new Error(req.statusText)) } } req.onerror = function () { reject(new Error(req.statusText)) } req.onabort = function () { reject(new Error('abort this req')) } req.send() }) requestMap[URL] = { promise: promise, request: req } return promise } function abortPromise (promise) { if (typeof promise === 'undefined') { return } var request Object.keys(requestMap).some(function (URL) { if (requestMap[URL].promise === promise) { request = requestMap[URL].request return true } }) if (request != null && request.readyState !== XMLHttpRequest.UNSENT) { request.abort() } } module.exports.createXHRPromise = createXHRPromise module.exports.abortPromise = abortPromise
調用:
var cancelableXHR = require('./cancelableXHR') var xhrPromise = cancelableXHR.createXHRPromise('http://httpbin.org/get') // 建立包裝了XHR的promise對象 xhrPromise.catch(function (error) { // 調用 abort 拋出的錯誤 }) cancelableXHR.abortPromise(xhrPromise) // 取消在建立的promise對象的請求操做
Promise.all()能夠進行promise對象的並行處理,那麼怎麼實現串行處理呢?
咱們將處理內容統一放到數組裏,再配合for循環進行處理:
var request = { comment: function getComment () { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse) }, people: function getPeople () { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse) } } function main() { function recordValue(results, value) { results.push(value) return results } // [] 用來保存初始化值 var pushValue = recordValue.bind(null, []) // 返回promise對象的函數的數組 var tasks = [request.comment, request.people] var promise = Promise.resolve() // 開始的地方 for (var i = 0; i < tasks.length; i++) { var task = tasks[i] promise = promise.then(task).then(pushValue) } return promise } // 運行示例 main().then(function (value) { console.log(value) }).catch(function (error) { console.error(error) })
上面代碼中的promise = promise.then(task).then(pushValue)經過不斷對promise進行處理,不斷的覆蓋promise變量的值,以達到對promise對象的累積處理效果。
可是這種方法須要promise這個臨時變量,從代碼質量上來講顯得不那麼簡潔。咱們可使用Array.prototype.reduce來優化main函數:
function main() { function recordValue (results, value) { results.push(value) return results } var pushValue = recordValue.bind(null, []) var tasks = [request.comment, request.people] return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue) }, Promise.resolve()) }
實際上咱們能夠提煉出進行順序處理的函數:
function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value) return results } var pushValue = recordValue.bind(null, []) return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue) }, Promise.resolve()) }
這樣咱們只要以下調用,代碼也更加清晰易懂了:
var request = { comment: function getComment() { return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse) }, people: function getPeople() { return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse) } } function main() { return sequenceTasks([request.comment, request.people]) } // 運行示例 main().then(function (value) { console.log(value) }).catch(function (error) { console.error(error) })
下面的內容來自google開發社區的一篇關於promise的文章JavaScript Promise:簡介
假設咱們要根據story.json經過ajax獲取章節內容,每一次ajax只能獲取一節內容。那麼怎麼作到又快又能按序展現章節內容呢?即若是第一章下載完後,咱們可將其添加到頁面。這可以讓用戶在其餘章節下載完畢前先開始閱讀。若是第三章比第二章先下載完後,咱們不將其添加到頁面,由於還缺乏第二章。第二章下載完後,咱們可添加第二章和第三章,後面章節也是如此添加。
前一節的串行方法只能一個ajax請求task處理完後再去執行下一個task,而Promise.all()能同時請求,可是隻有所有請求結束後才能獲得有序的數組。
具體實現請看下面實例。
咱們可使用JSON來同時獲取全部章節,而後建立一個向文檔中添加章節的順序。
story.json以下:
{ "heading": "<h1>A story about something</h1>", "chapterUrls": [ "chapter-1.json", "chapter-2.json", "chapter-3.json", "chapter-4.json", "chapter-5.json" ] }
具體處理代碼:
function getJson(url) { return get(url).then(JSON.parse) } getJSON('story.json') .then(function (story) { addHtmlToPage(story.heading) // 文章頭部添加到頁面 // 將拿到的chapterUrls數組map爲json promises數組,這樣能夠保證並行下載 return story.chapterUrls .map(getJSON) .reduce(function(sequence, chapterPromise) { // 用reduce方法鏈式調用promises,並將每一個章節的內容到添加頁面 return sequence.then(function () { // 等待獲取當前準備插入頁面的順序的資源,而後等待這個順序對應章節的成功請求 // Wait for everything in the sequence so far, then wait for this chapter to arrive. return chapterPromise }).then(function(chapter) { addHtmlToPage(chapter.html) // 將章節內容到添加頁面 }) }, Promise.resolve()) }) .then(function() { addTextToPage('All done') // 頁面添加All done文字 }) .catch(function(err) { // catch錯誤信息 addTextToPage('Argh, broken: '' + err.message) }) .then(function() { document.querySelector('.spinner').style.display = 'none' // 關閉加載提示 })
在Promise中你能夠將then和catch等方法連在一塊兒寫。這很是像DOM或者jQuery中的鏈式調用。
通常的方法鏈都經過返回this將多個方法串聯起來。
那麼怎麼在不改變已有采用了方法鏈編寫的代碼的外部接口的前提下,如何在內部使用Promise進行重寫呢?
1)fs中的方法鏈
以Node.js中的fs爲例。
此外,這裏的例子咱們更重視代碼的易理解性,所以從實際上來講這個例子可能並不算太實用。
有fs-method-chain.js:
'use strict' var fs = require('fs') function File() { this.lastValue = null } // Static method for File.prototype.read File.read = function FileRead(filePath) { var file = new File() return file.read(filePath) } File.prototype.read = function (filePath) { this.lastValue = fs.readFileSync(filePath, 'utf-8') return this } File.prototype.transform = function (fn) { this.lastValue = fn.call(this, this.lastValue) return this } File.prototype.write = function (filePath) { this.lastValue = fs.writeFileSync(filePath, this.lastValue) return this } module.exports = File
調用:
var File = require('./fs-method-chain') var inputFilePath = 'input.txt', outputFilePath = 'output.txt' File.read(inputFilePath) .transform(function (content) { return '>>' + content }) .write(outputFilePath)
2)基於Promise的fs方法鏈
下面咱們就在不改變剛纔的方法鏈對外接口的前提下,採用Promise對內部實現進行重寫。
'use strict' var fs = require('fs') function File() { this.promise = Promise.resolve() } // Static method for File.prototype.read File.read = function (filePath) { var file = new File() return file.read(filePath) } File.prototype.then = function (onFulfilled, onRejected) { this.promise = this.promise.then(onFulfilled, onRejected) return this } File.prototype['catch'] = function (onRejected) { this.promise = this.promise.catch(onRejected) return this } File.prototype.read = function (filePath) { return this.then(function () { return fs.readFileSync(filePath, 'utf-8') }) } File.prototype.transform = function (fn) { return this.then(fn) } File.prototype.write = function (filePath) { return this.then(function (data) { return fs.writeFileSync(filePath, data) }) } module.exports = File
3)二者的區別
要說fs-method-chain.js和Promise版二者之間的差異,最大的不一樣那就要算是同步和異步了。
若是在相似fs-method-chain.js的方法鏈中加入隊列等處理的話,就能夠實現幾乎和異步方法鏈一樣的功能,可是實現將會變得很是複雜,因此咱們選擇了簡單的同步方法鏈。
Promise版的話如同以前章節所說只會進行異步操做,所以使用了promise的方法鏈也是異步的。
另外二者的錯誤處理方式也是不一致的。
雖然fs-method-chain.js裏面並不包含錯誤處理的邏輯,可是因爲是同步操做,所以能夠將整段代碼用try-catch包起來。
在Promise版提供了指向內部promise對象的then和catch別名,因此咱們能夠像其它promise對象同樣使用catch來進行錯誤處理。
若是你想在fs-method-chain.js中本身實現異步處理的話,錯誤處理可能會成爲比較大的問題;能夠說在進行異步處理的時候,仍是使用Promise實現起來比較簡單。
4)Promise以外的異步處理
若是你很熟悉Node.js的話,那麼看到方法鏈的話,你是否是會想起來Stream呢。
若是使用Stream的話,就能夠免去了保存this.lastValue的麻煩,還能改善處理大文件時候的性能。 另外,使用Stream的話可能會比使用Promise在處理速度上會快些。
所以,在異步處理的時候並非說Promise永遠都是最好的選擇,要根據本身的目的和實際狀況選擇合適的實現方式。
5)Promise wrapper
再回到fs-method-chain.js和Promise版,這兩種方法相比較內部實現也很是相近,讓人以爲是否是同步版本的代碼能夠直接就當作異步方式來使用呢?
因爲JavaScript能夠向對象動態添加方法,因此從理論上來講應該能夠從非Promise版自動生成Promise版的代碼。(固然靜態定義的實現方式容易處理)
儘管ES6 Promises並無提供此功能,可是著名的第三方Promise實現類庫bluebird等提供了被稱爲Promisification的功能。
若是使用相似這樣的類庫,那麼就能夠動態給對象增長promise版的方法。
var fs = Promise.promisifyAll(require('fs')) fs.readFileAsync('myfile.js', 'utf8').then(function (contents) { console.log(contents) }).catch(function (e) { console.error(e.stack) })
前面的Promisification具體都幹了些什麼光憑想象恐怕不太容易理解,咱們能夠經過給原生 Array增長Promise版的方法爲例來進行說明。
在JavaScript中原生DOM或String等也提供了不少建立方法鏈的功能。Array中就有諸如map和filter等方法,這些方法會返回一個數組類型,能夠用這些方法方便的組建方法鏈。
'use strict' function ArrayAsPromise (array) { this.array = array this.promise = Promise.resolve() } ArrayAsPromise.prototype.then = function (onFulfilled, onRejected) { this.promise = this.promise.then(onFulfilled, onRejected) return this } ArrayAsPromise.prototype['catch'] = function (onRejected) { this.promise = this.promise.catch(onRejected) return this } Object.getOwnPropertyNames(Array.prototype).forEach(function (methodName) { // Don't overwrite if (typeof ArrayAsPromise[methodName] !== 'undefined') { return } var arrayMethod = Array.prototype[methodName] if (typeof arrayMethod !== 'function') { return } ArrayAsPromise.prototype[methodName] = function () { var that = this var args = arguments this.promise = this.promise.then(function () { that.array = Array.prototype[methodName].apply(that.array, args) return that.array }) return this } }) module.exports = ArrayAsPromise module.exports.array = function newArrayAsPromise (array) { return new ArrayAsPromise(array) }
原生的Array和ArrayAsPromise在使用時有什麼差別呢?咱們能夠經過對上面的代碼進行測試來了解它們之間的不一樣點。
'use strict' var assert = require('power-assert') var ArrayAsPromise = require('../src/promise-chain/array-promise-chain') describe('array-promise-chain', function () { function isEven(value) { return value % 2 === 0 } function double(value) { return value * 2 } beforeEach(function () { this.array = [1, 2, 3, 4, 5] }) describe('Native array', function () { it('can method chain', function () { var result = this.array.filter(isEven).map(double) assert.deepEqual(result, [4, 8]) }) }) describe('ArrayAsPromise', function () { it('can promise chain', function (done) { var array = new ArrayAsPromise(this.array) array.filter(isEven).map(double).then(function (value) { assert.deepEqual(value, [4, 8]) }).then(done, done) }) }) })
咱們看到,在ArrayAsPromise中也能使用Array的方法。原生的Array是同步處理,而ArrayAsPromise則是異步處理。
仔細看一下ArrayAsPromise的實現,也許你已經注意到了,Array.prototype的全部方法都被實現了。可是,Array.prototype中也存在着相似array.indexOf等並不會返回數組類型數據的方法,這些方法若是也要支持鏈式調用的話就有些不天然了。
在這裏很是重要的一點是,咱們能夠經過這種方式,爲具備接收相同類型數據接口的API動態的建立Promise版的API。若是咱們能意識到這種API的規則性的話,那麼就可能發現一些新的使用方法。
剖析Promise內部結構,一步一步實現一個完整的、能經過全部Test case的Promise類
關於反面模式,維基百科是這樣定義的:在軟件工程中,一個反面模式(anti-pattern或antipattern)指的是在實踐中明顯出現但又低效或是有待優化的設計模式,是用來解決問題的帶有共同性的不良方法。
Promise中常見的反面模式有嵌套的promise、沒有正確error handle等。
We have a problem with promises原文
We have a problem with promises中文翻譯
1)使用async/await
async/await更增強大,能寫出更像同步的代碼。可是基礎仍然是要掌握Promise。
2)使用Rxjs(Angular2後框架自帶)。