Promise
是我最喜歡的es6語法,也是面試中最容易問到的部分。那麼怎麼作到在使用中駕輕就熟,在面試中脫穎而出呢?
先來個面試題作作:html
面試官常常會讓手寫一個Promise封裝,寫出下面這一版就好了(想了解更多的可自行擴展):java
function ajaxMise(url, method, data, async, timeout) {
var xhr = new XMLHttpRequest()
return new Promise(function (resolve, reject) {
xhr.open(method, url, async);
xhr.timeout = options.timeout;
xhr.onloadend = function () {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
resolve(xhr);
else
reject({
errorType: 'status_error',
xhr: xhr
})
}
xhr.send(data);
//錯誤處理
xhr.onabort = function () {
reject(new Error({
errorType: 'abort_error',
xhr: xhr
}));
}
xhr.ontimeout = function () {
reject({
errorType: 'timeout_error',
xhr: xhr
});
}
xhr.onerror = function () {
reject({
errorType: 'onerror',
xhr: xhr
})
}
})
}
複製代碼
Promise
是一個對象,保存着將來將要結束的事件。她有兩個特徵,引用阮一峯老師的描述就是:react
(1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變。
(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。es6
let promise1 = new Promise(function (resolve, reject){
setTimeout(function (){
resolve('ok') //將這個promise置爲成功態(fulfilled),會觸發成功的回調
},1000)
})
promise1.then(fucntion success(val) {
console.log(val) //一秒以後會打印'ok'
})
複製代碼
class PromiseM {
constructor (process) {
this.status = 'pending'
this.msg = ''
process(this.resolve.bind(this), this.reject.bind(this))
return this
}
resolve (val) {
this.status = 'fulfilled'
this.msg = val
}
reject (err) {
this.status = 'rejected'
this.msg = err
}
then (fufilled, reject) {
if(this.status === 'fulfilled') {
fufilled(this.msg)
}
if(this.status === 'rejected') {
reject(this.msg)
}
}
}
//測試代碼
var mm=new PromiseM(function(resolve,reject){
resolve('123');
});
mm.then(function(success){
console.log(success);
},function(){
console.log('fail!');
});
複製代碼
上面提到Promise
和事件的不一樣,除此以外還有一個重要不一樣,就是Promise
建立是micro-task
。再看一道面試題:面試
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
console.log('script end');
複製代碼
正確答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。緣由就是:ajax
Event Loop
是js的一個重要機制,就是遇到事件或者setTimeout
等就會把對應的回調函數放入一個事件隊列(task queue),等到主程序執行完畢就依次把隊列裏的函數壓入棧中執行。能夠參考阮一峯老師的JavaScript 運行機制詳解:再談Event Loop,不過貌似老師的網站被攻擊尚未恢復。
可是Promise
不是上面的機制,她建立的是一個微任務(micro-task),micro-task
的執行老是在當前執行棧結束和下一個task
執行以前,順序就是「當前執行棧」 -> 「micro-task」 -> 「task queue中取一個回調」 -> 「micro-task」 -> ... (不斷消費task queue) -> 「micro-task」,總之就是當前執行棧爲空時,就到了一個micro-task
的檢查點。
下面是micro-task
的定義:數據庫
Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
編程
Promise
註冊的是micro-task
,因此上面題目中:主線程中'script start'、'script end'先打印,而後清空微任務隊列,'promise1'、'promise2'打印,而後取出task queue
中的回調執行,'setTimeout'打印。後端
Promise
提供了對js異步編程的新的解決方案,由於咱們一直使用的回調函數實際上是存在很大問題,只是限制於js的單線程等緣由不得不大量書寫。固然Promise
並非徹底擺脫回調,她只是改變了傳遞迴調的位置。那麼傳統的回調存在什麼問題呢?promise
這裏所說的嵌套是指大量的回調函數會使得代碼難以讀懂和修改,試想一個這個場景:讓你把下面的url4的調用提到url2以前。你須要很是當心的剪切代碼,而且笨拙的粘貼,result4這個參數你還不敢修改,由於這要額外花費不少功夫而且存在風險。
$.ajax('url1',function success(result1){
$.ajax('url2',function success(result2){
$.ajax('url3',function success(result3){
$.ajax('url4',function success(result4){
//……
})
})
})
})
複製代碼
固然,上面的問題有點戲劇成分,現實中極少出現這種難搞的狀況。與此相比,回調函數帶來的思惟上的難以理解是更致命的,由於咱們的大腦更喜歡同步的邏輯,這也是爲何await
關鍵字那麼受歡迎的緣由。
我記得有一次我給後端的同窗作JS新特性分享的時候,說到await
關鍵字,有我的驚呼:「哇!這個不錯啊,這就能夠像寫java同樣寫代碼了」。
除去書寫的不優雅和維護的困難之外,回調函數其實還存在信任問題。
事實上回調函數不必定會像你指望的那樣被調用。由於控制權不在你的手上。這種問題被稱做「控制反轉」。例以下面的例子:
$.ajax('xxxxxx',function success(result1){
//好比成功以後我會操做數據庫記錄結算金額
})
複製代碼
上面是jQuery
中的ajax
調用,咱們指望在某些事件結束後,讓第三方(jQ)幫咱們執行個人程序(回調)。
那麼,咱們和第三方之間並無一個契約或者規範能夠遵循,除非你把你想使用的第三方庫通讀一遍,保證它作了你想作的事,但事實上你很難肯定。即便在本身的代碼中,或者本身編寫的工具,咱們都很難作到百分之百信任。
Promise
是一個規範,嘗試以一種更加友好的方式書寫代碼。Promise
對象接受一個函數做爲參數,函數提供兩個參數:
promise
從未完成切換到成功狀態,也就是上面提到的從pending
切換到fufilled
,resolve
能夠傳遞參數,下一級promise
中的成功函數會接收到它promise
從未完成切換到失敗狀態,即從pending
切換到rejected
let promise1 = new Promise(function(reslove, reject){
//reslove或者reject或者出錯
})
promise1.then(fufilled, rejected).then().then() //這是僞代碼
promise1.then(fufilled, rejected)//能夠then屢次
function fufilled(data) {
console.log(data)
}
function rejected(e){
console.log(e)
}
複製代碼
正如上面提到的兩個特徵,一旦狀態改變,這個Promise
就已經完成決議(不會再更改),而且返回一個新的Promise
,能夠鏈式調用。而且能夠註冊多個then
方法,他們同時決議而且互不影響。這種設計明顯比回調函數要優雅的多,也更易於理解和維護。那麼在信任問題上她又有哪些改善呢?
Promise
經過通知的機制將「控制反轉」的關係又「反轉」回來。回調是我傳遞給第三方一個函數,指望它在事件發生時幫我執行,而Promise
是在你們都遵循規範的前提下,我會在事件發生時獲得通知,這時我決定作一些事(執行一些函數)。看到了吧,這是有本質差別的。
此外,回調函數還有如下信任問題,Promise
也都作了相關約束:
一個Promise
回調必定會在當前棧執行完畢和下一個異步時機點上調用,即便像下面這樣的同步resolve
代碼也會異步執行,而你傳給工具庫的回調函數卻可能被同步執行(調用過早)或者被忘記執行(或者過晚)。
new Promise(function (resolve) {
resolve(111111);
})
複製代碼
Promise
只能被決議一次,若是你屢次決議,她只會執行第一次決議,例如:
new Promise(function (reslove, reject) {
resolve()
setTimeout(function () {
resolve(2)
},1000)
resolve(3)
}).then(function (val) {
console.log(val) //undefined
})
複製代碼
成功回調的參數是經過resolve
傳遞的,例如像上面的代碼同樣,沒有傳遞參數,那麼val收到的會是undefined
,因此,不管如何都會收到參數。注意:resolve
只接收一個參數,以後的參數會被忽略。
Promise
的錯誤處理機制是這樣的:若是顯示的調用reject
並傳遞錯誤理由,這個消息會傳遞給拒絕回調。
此外,若是任意過程當中出現錯誤(例如TypeError或者ReferenceError),這個錯誤會被捕捉,而且使這個Promise
拒絕,也就是說這個錯誤消息也會傳遞給拒絕回掉,這與傳統的回調是不一樣的,傳統的回調一旦出錯會引發同步相應,而不出錯則是異步。
all
和race
兩個函數都是併發執行promise
的方法,他們的返回值也是promise
,all
會等全部的promise
都決議以後決議,而race
是隻要有一個決議就會決議。
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
複製代碼
注意:若是參數爲空,
all
方法會馬上決議,而race
方法會掛住。
Promise.all = function(ary) {
let num = 0
let result = []
return new Promise(function(reslove, reject){
ary.forEach(promise => {
promise.then(function(val){
if(num >= ary.length){
reslove(result)
}else{
result.push(val)
num++
}
},function(e){
reject(e)
})
})
})
}
複製代碼
你肯能會想到 instanceof Promise
,但遺憾的是不能夠。緣由是每種環境都封裝了本身的Promise
,而不是使用原生的ES6 Promise
。
因此目前判斷Promise
的一種方法就是判斷它是否是thenable
對象(若是它是一個對象或者函數,而且它具備then
方法)。
這是一種js常見的類型檢測方法——鴨子類型檢測:
鴨子類型檢測:若是它看起來像鴨子,叫起來也像鴨子,那麼它就是鴨子
resolve
返回一個當即成功的Promise
,reject
返回一個當即失敗的Promise
,他們是new Promise
的語法糖,因此下面兩個寫法是等價的:
let p1 = new Promise(function(resolve, reject){
reslove(11111)
})
let p2 = Promise.resolve(11111) //這和上面的寫法結果同樣
複製代碼
此外,若是傳入reslove
方法的參數不是promise
而是一個thenable
值,那麼reslove
會將它展開。最終的決議值由then
方法來決定。
上面提到,Promise
是異步處理錯誤,也就是說個人錯誤要在下一個Promise
才能捕獲到,大多狀況這是好的,可是存在一個問題:若是捕獲錯誤的代碼再出現錯誤呢?
個人作法一般是在代碼的最後加catch
:
let p1 = new Promise(function(reslove, reject){
ajax('xxxxx')
})
p1
.then(fullfilled, rejected)
.then(fullfilled, rejected)
.catch(function(e){
//處理錯誤
})
複製代碼
文章到這裏就結束了,若是你看完了而且所以思考了一些東西,我很高興。
接下來會繼續更新Promise
+generator
、異步函數等Promise
相關知識,願共同進步。