若是你已經對JavaScript異步有必定了解,或者已經閱讀過本系列的其餘兩篇文章,那請繼續閱讀下一小節,若你還有疑惑或者想了解JavaScript異步機制與編程,能夠閱讀一遍這兩篇文章:前端
回調函數,做爲JavaScript異步編程的基本單元,很是常見,你確定對下面這類代碼一點都不陌生:面試
component.do('purchase', funcA); function funcA(args, callback) { //... setTimeout(function() { $.ajax(url, function(res) { if (res) { callback(res) } else {//...} }); }, 300); funcB(); setTimeout(function() { $.ajax(arg, function(res) { if (res) { callback(res); } }); }, 400); }
上面這些代碼,一層一層,嵌套在一塊兒,這種代碼一般稱爲回調地獄,不管是可讀性,仍是代碼順序,或者回調是否可信任,亦或是異常處理角度看,都是不盡人意的,下面作簡單闡述。ajax
上文例子中代碼funcB
函數,還有兩個定時器回調函數,回調內各自又有一個ajax異步請求而後在請求回調裏面執行最外層傳入的回調函數,對於這類代碼,你是否能明確指出個回調的執行順序呢?若是funcB
函數內還有異步任務呢?,狀況又如何?編程
假如某一天,好比幾個月後,線上出了問題,咱們須要跟蹤異步流,找出問題所在,而跟蹤這類異步流,不只須要理清個異步任務執行順序,還須要在衆多回調函數中不斷地跳躍,調試(或許你還能記得諸如funcB
這些函數的做用和實現),不管是出於效率,可讀性,仍是出於人性化,都不但願開開發者們再經歷這種痛苦。數組
如上,咱們調用了一個第三方支付組件的支付API,進行購買支付,正常狀況發現一切運行良好,可是假如某一天,第三方組件出問題了,可能屢次調用傳入的回調,也可能傳回錯誤的數據。說到底,這樣的回調嵌套,控制權在第三方,對於回調函數的調用方式、時間、次數、順序,回調函數參數,還有下一節將要介紹的異常和錯誤都是不可控的,由於不管如何,並不總能保證第三方是可信任的。promise
關於JavaScript錯誤異常,初中級開發接觸的可能並很少,可是其實仍是有不少能夠學習實踐的地方,如前端異常監控系統的設計,開發和部署,並非三言兩語能闡述的,以後會繼續推出相關文章。瀏覽器
咱們知道當JavaScript拋出錯誤或異常時,對於未捕獲異常,瀏覽器會默認在控制檯輸出錯誤堆棧信息,以下,當test
未定義時:異步
function init(name) { test(name) } init('jh');
輸出如圖:async
如圖中自頂向下輸出紅色異常堆棧信息,Uncaught
表示該異常未捕獲,ReferenceError
代表該異常類型爲引用異常,冒號後是異常的詳細信息:test is not defined
,test
未定義;後面以at
起始的行就是該異常發生處的調用堆棧。第一行說明異常發生在init
函數,第二行說明init
函數的調用環境,此處在控制檯直接調用,即至關於在匿名函數環境內調用。異步編程
上面例子是同步代碼執行的異常,當異常發生在異步任務內時,又會如何呢?,假如把上例中代碼放在一個setTimeout
定時器內執行:
function init(name) { test(name) } setTimeout(function A() { setTimeout(function() { init(); }, 0); }, 0);
如圖:
能夠看到,異步任務中的未捕獲異常,也會在控制檯輸出,可是setTimeout
異步任務回調函數沒有出如今異常堆棧,爲何呢?這是由於當init
函數執行時,setTimeout
的異步回調函數不在執行棧內,而是經過事件隊列調用。
JavaScript的異常捕獲,主要有兩種方式:
try{}catch(e){}主動捕獲異常;
如上,對於同步執行大代碼出現異常,try{}catch(e){}
是能夠捕獲的,那麼異步錯誤呢?
如上圖,咱們發現,異步回調中的異常沒法被主動捕獲,由瀏覽器默認處理,輸出錯誤信息。
window.onerror事件處理器,全部未捕獲異常都會自動進入此事件回調
如上圖,輸出了script error
錯誤信息,同時,你也許注意到了,控制檯依然打印出了錯誤堆棧信 息,或許你不但願用戶看到這麼醒目的錯誤提醒,那麼可使window.onerror
的回調返回true便可阻止瀏覽器的默認錯誤處理行爲:
固然,通常不隨意設置window.onerror回調,由於程序一般可能須要部署前端異常監控系統,而一般就是使用window.onerror處理器實現全局異常監控,而該事件處理器只能註冊一個回調。
以上咱們談到的諸多關於回調的不足,都很常見,因此必須是須要解決的,而Promise正是一種很好的解決這些問題的方式,固然,如今已經提出了比Promise更先進的異步任務處理方式,可是目前更大範圍使用,兼容性更好的方式仍是Promise,也是本篇要介紹的,以後會繼續介紹其餘處理方式。
分析了一大波問題後,咱們知道Promise的目標是異步管理,那麼Promise究竟是什麼呢?
因此,Promise是一種封裝將來值的易於複用的異步任務管理機制。
爲了更好的理解Promise,咱們介紹一下Promises/A+,一個公開的可操做的Promises實現標準。先介紹標準規範,再去分析具體實現,更有益於理解。
Promise表明一個異步計算的最終結果。使用promise最基礎的方式是使用它的then
方法,該方法會註冊兩個回調函數,一個接收promise完成的最終值,一個接收promise被拒絕的緣由。
你可能還會想問Promises/A是什麼,和Promises/A+有什麼區別。Promises/A+在Promises/A議案的基礎上,更清晰闡述了一些準則,拓展覆蓋了一些事實上的行爲規範,同時刪除了一些不足或者有問題的部分。
Promises/A+規範目前只關注如何提供一個可操做的then
方法,而關於如何建立,決議promises是往後的工做。
then
方法的對象;then
方法的對象;undefined
,thenable
對象,promise
對象;一個promise只可能處於三種狀態之一:
這三個狀態變動關係需知足如下三個條件:
一個promise必須提供一個then
方法,以供訪問其當前狀態,或最終值或拒絕緣由。
參數
該方法接收兩個參數,如promise.then(onFulfilled, onRejected)
:
返回值
該方法必須返回一個promise:
var promise2 = promise1.then(onFulfilled, onRejected); // promise2依然是一個promise對象
決議是一個抽象操做過程,該操做接受兩個輸入:一個promise和一個值,能夠記爲;[[resolve]](promise, x)
,若是x是一個thenable
對象,則嘗試讓promise
參數使用x
的狀態值;不然,將使用x
值完成傳入的promise
,決議過程規則以下:
promise
和x
引用自同一對象,則使用一個TypeError
緣由拒絕此promise
;x
爲Promise,則promise
直接使用x
的狀態;x
爲對象或函數:
x.then
的引用;x.then
時拋出異常e
,使用該e
做爲緣由拒絕promise
;then
;then
是一個函數,就調用該函數,其做用域爲x
,並傳遞兩個回調函數參數,第一個是resolvePromise
,第二個是rejectPromise
:
resolvePromise(y)
,則執行resolve(promise, y)
;rejectPrtomise(r)
,則使用緣由r
拒絕promise
;then
拋出異常e
,則:
promise
已決議,即調用了resolvePromise
或rejectPrtomise
,則忽略此異常;e
拒絕promise
;then
不是函數,則使用x
值完成promise
;x
不是對象或函數,則使用x
完成promise
。天然,以上規則可能存在遞歸循環調用的狀況,如一個promsie
被一個循環的thenable
對象鏈決議,此時天然是不行的,因此規範建議進行檢測,是否存在遞歸調用,若存在,則以緣由TypeError
拒絕promise
。
在ES6中,JavaScript已支持Promise,一些主流瀏覽器也已支持該Promise功能,如Chrome,先來看一個Promsie使用實例:
var promise = new Promise((resolve, reject) => { setTimeout(function() { resolve('完成'); }, 10); }); promise.then((msg) => { console.log('first messaeg: ' + msg); }) promise.then((msg) => { console.log('second messaeg: ' + msg); });
輸出以下:
建立promise語法以下:
new Promise(function(resolve, reject) {});
參數
一個函數,該函數接受兩個參數:resolve函數和reject函數;當實例化Promise構造函數時,將當即調用該函數,隨後返回一個Promise對象。一般,實例化時,會初始一個異步任務,在異步任務完成或失敗時,調用resolve或reject函數來完成或拒絕返回的Promise對象。另外須要注意的是,若傳入的函數執行拋出異常,那麼這個promsie將被拒絕。
all方法接受一個或多個promsie(以數組方式傳遞),返回一個新promise,該promise狀態取決於傳入的參數中的全部promsie的狀態:
var p1 = new Promise((resolve, reject) => { setTimeout(function(){ console.log('p1決議'); resolve('p1'); }, 10); }); var p2 = new Promise((resolve, reject) => { setTimeout(function(){ console.log('p2決議'); resolve('p2'); }, 10); }); Promise.all( [p1, p2] ) .then((msgs) => { // p1和p2完成並傳入最終值 console.log(JSON.stringify(msgs)); }) .then((msg) => { console.log( msg ); });
輸出以下:
race方法返回一個promise,只要傳入的諸多promise中的某一個完成或被拒絕,則該promise一樣完成或被拒絕,最終值或拒絕緣由也與之相同。
resolve方法返回一個已決議的Promsie對象:
x
是一個promise或thenable
對象,則返回的promise對象狀態同x
;x
不是對象或函數,則返回的promise對象以該值爲完成最終值;該方法遵循Promise/A+決議規範。
返回一個使用傳入的緣由拒絕的Promise對象。
該方法爲promsie添加完成或拒絕處理器,將返回一個新的promise,該新promise接受傳入的處理器調用後的返回值進行決議;若promise未被處理,如傳入的處理器不是函數,則新promise維持原來promise的狀態。
咱們經過兩個例子介紹then
方法,首先看第一個實例:
var promise = new Promise((resolve, reject) => { setTimeout(function() { resolve('完成'); }, 10); }); promise.then((msg) => { console.log('first messaeg: ' + msg); }).then((msg) => { console.log('second messaeg: ' + msg); });
輸出以下:
輸出兩行信息:咱們發現第二個then
方法接收到的最終值是undefined
,爲何呢?看看第一個then
方法調用後返回的promise
狀態以下:
如上圖,發現調用第一個then
方法後,返回promise最終值爲undefined
,傳遞給第二個then
的回調,若是把上面的例子稍加改動:
var promise = new Promise((resolve, reject) => { setTimeout(function() { resolve('完成'); }, 10); }); promise.then((msg) => { console.log('first messaeg: ' + msg); return msg + '第二次'; }).then((msg) => { console.log('second messaeg: ' + msg); });
輸出以下:
此次兩個then
方法的回調都接收到了最終值,正如咱們前文所說,'then'方法返回一個新promise,而且該新promise根據其傳入的回調執行的返回值,進行決議,而函數未明確return
返回值時,默認返回的是undefined
,這也是上面實例第二個then
方法的回調接收undefined
參數的緣由。
這裏使用了鏈式調用,咱們須要明確:共產生三個promise,初始promise,兩個then方法分別返回一個promise;而第一個then
方法返回的新promise是第二個then
方法的主體,而不是初始promise。
該方法爲promise添加拒絕回調函數,將返回一個新promise,該新promise根據回調函數執行的返回值進行決議;若promise決議爲完成狀態,則新promise根據其最終值進行決議。
var promise = new Promise((resolve, reject) => { setTimeout(() => { reject('failed'); }, 0); }); var promise2 = promise.catch((reason) => { console.log(reason); return 'successed'; }); var promise3 = promise.catch((reason) => { console.log(reason); }); var promise4 = promise.catch((reason) => { console.log(reason); throw 'failed 2'; });
輸出以下圖:
如圖中所輸出內容,咱們須要明白如下幾點:
catch
會爲promise
註冊拒絕回調函數,一旦異步操做結束,調用了reject
回調函數,則依次執行註冊的拒絕回調;then
方法類似,catch
方法返回的新promise將使用其回調函數執行的返回值進行決議,如promise2,promise3狀態均爲完成(resolved),可是promise3最終值爲undefined
,而promise2
最終值爲successed
,這是由於在調用promise.catch
方法時,傳入的回調沒有顯式的設置返回值;catch
方法時,回調中throw
拋出異常,因此promise4狀態爲拒絕(rejected),拒絕緣由爲拋出的異常;then
方法,仍是catch
方法,都會返回一個新promise,此新promise與初始promise相互獨立。catch
方法和then
方法的第二個參數同樣,都是爲promise註冊拒絕回調。
和jQuery的鏈式調用同樣,Promise設計也支持鏈式調用,上一步的返回值做爲下一步方法調用的主體:
new Promise((resolve, reject) => { setTimeout(()=>{ resolve('success'); },0); }).then((msg) => { return 'second success'; }).then((msg) => { console.log(msg); });
最後輸出:second success
,初始化promise做爲主體調用第一個then
方法,返回完成狀態的新promise其最終值爲second success
,而後該新promise做爲主體調用第二個then
方法,該方法返回第三個promise,並且該promise最終值爲undefined
,若不清楚爲何,請回到關於Promise.prototype.then
和Promise.prototype.catch
的介紹。
咱們前文提到了JavaScript異步回調中的異常是難以處理的,而Promise對異步異常和錯誤的處理是比較方便的:
var promise = new Promise((resolve, reject) => { test(); // 拋出異常 resolve('success'); // 被忽略 }); console.log(promise); promise.catch((reason) => { console.log(reason); });
輸出如圖,執行test
拋出異常,致使promise被拒絕,拒絕緣由即拋出的異常,而後執行catch
方法註冊的拒絕回調:
目前爲止,關於Promise是什麼,咱們應該有了必定的認識,這裏,須要再次說明的是Promise的三個重要概念及其關係:決議(resolve),完成(fulfill),拒絕(reject)。
Promise.resolve
描述的就是一個決議過程,而Promise構造函數,傳入的回調函數的兩個參數:resolve和reject,一個是完成函數,一個是拒絕函數,這裏使人疑惑的是爲何這裏依然使用resolve而不是fulfill,咱們經過一個例子解釋這個問題:var promise = new Promise((resolve, reject) => { resolve(Promise.reject('failed')); }); promise.then((msg) => { console.log('完成:' + msg); }, (reason) => { console.log('拒絕:' + reason); });
輸出如圖:
上例中,在建立一個Promise時,給resolve
函數傳遞的是一個拒絕Promise,此時咱們發現promise狀態是rejected
,因此這裏第一個參數函數執行,完成的是一個更接近決議的過程(能夠參考前文講述的決議過程),因此命名爲resolve
是更合理的;而第二個參數函數,則只是拒絕該promise:
var promise = new Promise((resolve, reject) => { reject(Promise.resolve('success')); }); promise.then((msg) => { console.log('完成:' + msg); }, (reason) => { console.log('拒絕:' + reason); });
reject
函數並不會處理參數,而只是直接將其當作拒絕緣由拒絕promise。
Promise是什麼,怎麼樣使用就介紹到此,另一個問題是面試過程當中常常也會被說起的:如何實現一個Promise,固然,限於篇幅,咱們這裏只講思路,不會長篇大論。
首先建立一個構造函數,供實例化建立promise,該構造函數接受一個函數參數,實例化時,會當即調用該函數,而後返回一個Promise對象:
var MyPromise = (() => { var value = undefined; // 當前Promise var tasks = []; // 完成回調隊列 var rejectTasks = []; // 拒絕回調隊列 var state = 'pending'; // Promise初始爲等待態 // 輔助函數,使異步回調下一輪事件循環執行 var nextTick = (callback) => { setTimeout(callback, 0); }; // 輔助函數,傳遞Promsie的狀態值 var ref = (value) => { if (value && typeof value.then === 'function') { // 若狀態值爲thenable對象或Promise,直接返回 return value; } // 不然,將最終值傳遞給下一個then方法註冊的回調函數 return { then: function(callback) { return ref(callback(value)); } } }; var resolve = (val) => {}; var reject = (reason) => {}; function MyPromise(func) { func(resolve.bind(this), reject.bind(this)); } return MyPromise; });
在實例化建立Promise時,咱們會將構造函數的兩個靜態方法:resolve
和reject
傳入初始函數,接下來須要實現這兩個函數:
var resolve = (val) => { if (tasks) { value = ref(val); state = 'resolved'; // 將狀態標記爲已完成 // 依次執行任務回調 tasks.forEach((task) => { value = nextTick((val) => {task[0](self.value);}); }); tasks = undefined; // 決議後狀態不可變 return this; } }; var reject = (reason) => { if (tasks) { value = ref(reason); state = 'rejected'; // 將狀態標記爲已完成 // 依次執行任務回調 tasks.forEach((task) => { nextTick((reason) => {task[1](value);}); }); tasks = undefined; // 決議後狀態不可變 return this; } };
還有另外兩個靜態方法,原理仍是同樣,就不細說了。
目前構造函數,和靜態方法完成和拒絕Promise都已經實現,接下來須要考慮的是Promise的實例方法和鏈式調用:
MyPromise.prototype.then = (onFulfilled, onRejected) => { onFulfilled = onFulfilled || function(value) { // 默認的完成回調 return value; }; onRejected = onRejected || function(reason) { // 默認的拒絕回調 return reject(reason); }; if (tasks) { // 未決議時加入隊列 tasks.push(onFulfilled); rejectTasks.push(onRejected); } else { // 已決議,直接加入事件循環執行 nextTick(() => { if (state === 'resolved') { value.then(onFulfilled); } else if (state === 'rejected') { value.then(onRejected); } }); } return this; };
以上能夠簡單實現Promise部分異步管理功能:
var promise = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('完成'); }, 0); }); promise.then((msg) => {console.log(msg);});
本篇由回調函數起,介紹了回調處理異步任務的常見問題,而後介紹Promises/A+規範及Promise使用,最後就Promise實現作了簡單闡述(以後有機會會詳細實現一個Promise),花費一週終於把基本知識點介紹完,下一篇將介紹JavaScript異步與生成器實現。