寫在前面:
Promise這一章的順序對於未接觸過使用過Promise的童鞋而言略抽象了,前邊幾章主要爲了說明Promise和以前的異步方式相比有什麼優點和它能解決什麼問題,後邊才詳解Promise的API設計和各類場景下如何使用Promise。node
建議先了解和簡單使用過Promise後再閱讀,效果更佳。git
正文github
以前的方式:api
Promise方式:
第三方提供瞭解其任務什麼時候結束的能力數組
Promise的異步特性是基於任務的(圖示以下)
promise
一種處理異步的思路:爲了統一如今和未來,把它們都變成未來,即全部操做都成了異步的瀏覽器
書中關於Promise是個啥的觀點:安全
一種封裝和組合將來值的易於複用的機制
一種在異步任務中做爲兩個或更多步驟的流程控制機制,時序上的this-then-that —— 關注點分離
Promise設計的重要基礎併發
- Promise必定是異步執行的,即便是當即完成的Promise(相似 new Promise((resolve)=>{ resolve(42) })),也沒法被同步觀察到
- 一旦Promise決議,它就永遠保持在這個狀態,變成了不變值(immuatable value),這是設計中最基礎和最重要的因素
- Promise至多隻能有一個決議值(一個!一個!一個!)
引伸:異步
基於thenable的鴨子類型
if( p !== null && ( typeof p === 'object' || typeof p === 'function' ) && typeof p.then === 'function' ) { // 假定這是一個thenable } else { // 不是thenable }
這種方式顯然是有些問題的,可是目前通用的方式
信任問題見 異步篇
避免Zalgo這類反作用:一個任務有時同步完成,有時異步完成,可能致使競態條件
Promise從定義上保證了不會存在這種問題:參考3.1 設計基礎 — 即便是當即完成的Promise,也沒法被同步觀察到
Note: 調用過晚強調的是調用順序?
Promise建立對象調用resolve(..)或reject(..)時,這個Promise的then註冊的觀察回調就會自動調度(注意是被調度而不是執行) —— 在下一個異步時機點上依次被調用執行,它們相互之間是不會互相影響或延誤的
Promise一旦決議則必定會通知決議(傳入then的完成回調或拒絕回調調用),即便是Javascript運行錯誤也會調用拒絕回調
若是某個Promise一直不決議呢?使用競態的高級抽象機制:
// 超時工具 function timeoutPromise(delay){ return new Promise( (resolve, reject) => { setTimeout( function () { reject('Timeout!'); }, delay); } ) } // 設置某個Promise foo()超時 Promise.race( [ foo(), timeoutPromise(3000) ] ) .then( function () { // foo(..)及時完成 }, function (err) { // foo(..)被拒絕或者超時 // 經過查看err肯定錯誤狀況 } );
若是建立Promise的代碼試圖屢次調用resolve(..)或reject(..),或者二者都調用,Promise只會接受第一次決議,後續調用都會被忽略
Promise至多隻能有一個決議值
若是使用多個參數調用resolve(..)或reject(..),第一個參數以後的全部參數都會被忽略
Promise其實也是傳入回調函數,故函數中照樣能根據做用域規則訪問到對應的環境數據
這裏說的錯誤或異常可能出如今兩個過程:
查看其決議結果過程當中任什麼時候間點
,我的認爲可能翻譯得有點問題,應該要強調是其決議以前)這兩種錯誤都不會被丟棄,但針對它們的處理方式有所不一樣:
針對1:
該Promise會被當即拒絕,但注意這個異常也被變成了異步行爲
let p = new Promise ( function(resolve, reject){ foo.bar(); // foo undefined 將拋出錯誤 Promise=>reject resolve( 42 ); // 不會執行到這裏 }); p.then( function fulfilled(){ // 不會執行到這裏 }, function rejected(err){ // err是一個TypeError異常 } )
針對2:
這個時候當前Promise已經決議,其決議結果是個不可變值
then(..)調用返回的下一個Promise被拒絕
let q = new Promise ( function(resolve, reject){ resolve( 42 ); }) q.then( function fulfilled(){ foo.bar(); // foo undefined 將拋出錯誤 致使then返回的Promise被reject }, function rejected(err){ // 不會執行到這裏 } ).then( function fulfilled(){ // 不會執行到這裏 }, function rejected(err){ // err是一個TypeError異常 } )
Promise.resolve(..) 規範化傳入的值:
具體看例子(傳入Promise的狀況略)
// 傳入一個當即值 let p = Promise.resolve(42); p.then( res => { console.log('Promise.resolve(42).then:',res); }) let p1 = Promise.resolve({}); p1.then( res => { console.log('Promise.resolve({}).then:',res); }) // 傳入一個 thenable 嘗試展開 let p2 = Promise.resolve({ then: function(cb) { cb(42)} }); p2.then( res => { console.log('Promise.resolve(thenable).then:', res); }, err => { console.log('Promise.resolve(thenable).then:', err); }) // 注意 這種狀況其實也是當即值!!! let p3 = Promise.resolve( setTimeout(()=>{ return 'inside a continuation' },1000) ); // settimeout函數返回當前定時器引用=>耶 當即值 p3.then( res => { console.log('Promise.resolve(看起來是個異步).then:', res); })
Promise不只僅是一個單步執行this-then-that的操做機制,這只是它的構成部件,實際上Promise是能夠鏈接到一塊兒使用表示一系列異步步驟:
再仔細看看第二點,結合上文 3.3.7 Promise.resolve(..)的能力,這是Promise鏈式流在每一步都能有異步能力的關鍵!
栗子:
// 返回當即值 let p = Promise.resolve(21); p .then( function(v) { console.log(v); // 21 // 返回當即值 return v * 2; }) // 這裏是連接的Promise .then ( function(v) { console.log(v); // 42 });
// 返回Promise並引入異步 let p = Promise.resolve(21); p .then ( function(v) { // 返回一個異步Promise return new Promise( (resolve, reject) => { setTimeout(() => { resolve(v*2); }, 1000); }); }) .then ( function(v) { // 前一步延遲1s後執行 console.log(v); })
Promise鏈不只僅是一個表達多步異步序列的流程控制,還能夠從一個步驟到下一個步驟的消息通道
幾種錯誤處理方式:
try...catch結構不能應用於異步模式
function foo() { setTimeout(() => { baz.bar(); // 錯誤代碼 }, 100); } try{ foo(); // 以後將拋出全局錯誤 } catch (err) { // 不會走到這裏 }
foo()中有本身的異步完成函數,其中任何異步錯誤都沒法捕捉到
node.js api或庫中常見的err-first模式
function foo(cb) { setTimeout(() => { try { var x = baz.bar(); // 錯誤代碼 cb(null, x); } catch (err) { cb(err); } }, 100); } foo( function(err, val) { if(err) { console.error(err); // 報錯惹 } else { console.log(val); } })
分離回調模式(split-callback)
一個回調用於完成狀況,一個回調用於拒絕狀況
Promise採用的就是這種方式
先參考 3.3.6 再進行詳細討論:
Promise決議前、決議後產生的錯誤處理方式有所不一樣
錯誤的使用Promise API產生的錯誤會阻礙正常Promise對象的構造,這種狀況下會當即拋出異常(這種狀況應該死都不要出現 0 0)
因爲Promise鏈式特色,其鏈上的最後一步,不論是什麼,老是存在着在未被查看的Promise中出現未捕獲錯誤的可能性
即理論上來講:總有可能有錯誤未被捕獲,而出現全局報錯
P.S. 這也是我的認爲使用Promise最頭疼的一點
關於如何解決3.5.1提出問題的一些思路
該小節討論的是從做者角度提出一種避免在使用Promise時在開發者未注意的狀況下出現未捕獲錯誤而報出全局錯誤的方案
具體請看:
{ let p = Promise.reject(21); // 將觸發全局報錯 Uncaught (in promise) 21 let p1 = Promise.reject(21).then ( // 拒絕前,註冊了一個錯誤處理函數 (res) => { // 不會走到這裏 }, (err) => { console.log(`註冊了一個錯誤處理函數:${err}`); } ) Promise.prototype.defer = function (){ // 做者提出的一個API // 簡單實現就是單純的返回這個Promise自己 return this; } let p2 = Promise.reject(21).defer(); // p2的結果在未來會被查看,如今暫時不要報全局錯誤 let foo = Promise.resolve(21); foo .then (function(v) { return p2; // 這裏查看p2的結果 }, function (err) { // 不會走到這裏 }) .catch (function(v) { console.log(v); // p2的結果 }) }
基於Promise構建的異步抽象模式
相似門(gate)這種機制:須要等待兩個或更多並行/併發的任務都完成才能繼續,它們的完成順序並不重要,但必須都要完成,門才能打開並讓流程控制繼續
Promise.all([ .. ])的參數接收一個數組:
3.3.7 構建可信任的Promise
)返回一個Promise:
每一個promise都必須關聯一個拒絕/錯誤處理函數,特別是從Promise.all([ ... ])返回的那一個
相似門閂(shuan),競態:一旦有任何一個Promise決議爲完成,就標記爲完成;一旦有任何一個Promise決議爲拒絕,它就會拒絕
Promise.race([ ... ])的參數接收一個數組:
返回一個Promise:
關於這兩個API須要注意
在all和race中存在着被忽略或丟棄的promise,若是這些promise中保存着重要的數據或資源或者開發者須要記錄這些promise失敗的事實,又該怎麼辦呢?
finally API就是基於這種狀況提出的:Promise須要一個finally(...)回調註冊,這個回調在Promise決議後老是會被調用,並容許執行任何須要的清理工做
注:書中提到finally還未被規範支持,而在18年1月已經正式加入到提案中了,可參考 https://github.com/tc39/propo... 和 https://github.com/tc39/propo...
書中還提到了一種觀察模式(基於同一個Promise決議能夠被屢次查看),具體能夠看栗子
let foo = new Promise((resolve, reject) => { setTimeout(() => { resolve(21); }, 301); }); let timeout = function(time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve('timeout'); }, time); }) } // foo會被默默忽略 Promise.race( [ foo, timeout(300) ]) .then( (res) => { console.log(`Promise.race: ${res}`); }) .finally( (res) => { console.log(`Promise.race: ${res}`); // finally回調是不會提供任何參數的,詳情可看 https://github.com/tc39/proposal-promise-finally }) // 觀察者模式 if(!Promise.observe){ Promise.observe = function(pr, cb){ // 觀察pr的決議 pr.then( function fulfilled (msg){ // 完成時 Promise.resolve(msg).then(cb); }, function reject (msg){ // 拒絕時 傳遞錯誤消息 但注意觀察者promise是resolve的 Promise.resolve(msg).then(cb); } ); // 返回最初的promise return pr; } } // 仍是上一個超時的例子 Promise.race( [ Promise.observe( foo, function cleanup (msg){ console.log(`Promise.observe: ${msg}`); // foo即便沒有在超時以前完成 也能夠獲取其決議狀況 } ) .then ])
略
@TODO 自行實現 Promise.any finally map等擴展API
實現一個異步的map(..)工具
這裏也主要看栗子
if(!Promise.map) { Promise.map = function(vals, cb) { // 等待全部map的promise決議的新的promise return Promise.all( // 對vals使用map將每一個值轉出promise,值數組->Promise數組 vals.map( function(val){ // 將val值替換成調用cb函數後決議的新的promise return new Promise( function(resolve){ // resolve reject傳入到cb函數中 cb(val, resolve); }) }) ) } } // 使用Promise.map var p1 = Promise.resolve(21); var p2 = Promise.resolve(30); var p3 = Promise.reject('opps'); Promise.map( [p1,p2,p3], function(pr, resolve){ Promise.resolve(pr) .then( val => { resolve( val*2 ); }, resolve // 注意:不能發出拒絕信號,若是發出會致使Promise.map被拒絕,其餘map結果也會被丟棄 ) }) .then( (vals) => { console.log(vals); })
TODO:Promise API 概述詳解單獨成篇