Promise的前世今生
在js中,非同步是一個非常重要的組成部分,它基於事件迴圈,保證了優先順序更高任務的優先執行權,比如js下載、UI渲染、js中非非同步的任務,非同步使得單程式的js能夠做到非阻塞,這在node顯得攸關重要,它使得js不必等待I/O操作返回結果,而能去處理其他任務。但是非同步也存在著缺點,最明顯的就是回地獄,而Pormise規範的出現,讓開發者從回撥地獄從解放出來。它由社群提出和實現,之後被es6加入到標準當中。
Promise的組成
Promise最基本的是由status、resolve、onResolved、reject、onRejected、then和catch幾部分組成:
status:狀態管理,有pendding,fulfilled和rejected三種狀態,且狀態一經改變是無法逆轉的;
resolve: 一個函式,當呼叫該函式時,說明非同步任務執行成功了,promise的status由pendding轉為fulfilled,且會呼叫成功的回撥函式onResolved;
reject: 一個函式,當呼叫該函式時,說明非同步任務執行失敗,promise的status由pendding轉為rejected,且會呼叫失敗的回撥函式onRejected;
then: 一個函式,在then的引數裡面,我們需要定義回撥成功需要執行的成功回撥函式,promise會將這個成功回撥函式註冊到onResolved,由resolve觸發呼叫,並且還支援鏈式呼叫;
catch: 一個函式,在reject的引數裡面,我們需要回撥失敗需要執行的失敗回撥函式,promise會將這個失敗回撥函式註冊到onRejected,由reject觸發呼叫;
備註: 這裡沒講race和all方法等,有興趣的可以去看下文件。
使用方法
通過new建立一個Promise物件,傳入一個函式引數,這個函式包含resolve和reject引數,這兩個引數,如上所述也是函式,當非同步任務完成的時候,呼叫resolve方法,當非同步任務失敗的時候,呼叫reject方法。eg:
var p = new Promise(function(resolve, reject){
setTimeout(function() {
resolve(1)
}, 1000)
})
複製程式碼
then
方法,引數也是一個回撥函式,當你呼叫resolve方法後,這個回撥函式就會呼叫,且resolve函式傳入的引數,會帶給這個回撥函式的引數,eg:
p.then(function(arg){
console.log(arg) // 1
})
複製程式碼
catch
方法,引數同樣是一個回撥函式,當你呼叫reject方法後,這個回撥函式就會呼叫,且reject函式傳入的引數,會帶給這個回撥函式,使用例子和then同理。
所以我們的Promise的呼叫可以是這樣的:
var p = new Promise(function(resolve, reject){
// 非同步任務執行,且看結果呼叫resolve還是reject
}).then(function(){
// do something1
return new Promise...
}).then(function() {
// do something2
return new Promise....
}).then(function() {
// do something3
return new Promise....
}).then(function() {
// do something4
return new Promise....
})
複製程式碼
對比用回撥的方式
doAsyncTask1(function(){
// do something1
doAsyncTask2(function() {
// do something2
doAsyncTask3(function() {
// do something3
doAsyncTask4(function(){
// do something4
})
})
})
})
複製程式碼
是不是比起非同步程式設計,promise更人性化呢?
async await
上面promise的呼叫方式其實還不夠優雅,還有更加優雅的呼叫方式,那就是async await方式,我們來看下如何用。
async testPromise() {
var result = await new Promise(function(resolve, reject){
setTimeout(function(){
resolve(1)
}, 2000)
})
console.log(result)
var result1 = await new Promise(function(resolve, reject){
setTimeout(function(){
resolve(1)
}, 2000)
})
console.log(result1)
}
複製程式碼
是不是已經變成了我們同步的程式設計方式了?
這裡有兩點需要注意的:
- await關鍵字必須在使用async修飾的方法中才能使用,反之則沒問題。
- async不能直接使用,需要使用babel進行轉譯,一般需要藉助webpack進行配置,不能單獨在js中使用,這個相對來說比較麻煩。
如何實現自己實現Promise
面試官很喜歡問你如何實現一個Promise,這個時候我們就需要對Promise有一定的理解,我們不去看Promise/A+規範,太複雜晦澀難懂了,我們就按照上面講的Promise的組成來實現一個簡單的Promise。
function _Promise(fn) {
this.status = 'pending';
this.onResolved = null;
this.onRejected = null;
fn.call(this, this.resolve.bind(this), this.reject.bind(this))
}
_Promise.prototype.resolve = function(arg) {
if(this.status === 'pending') {
this.onResolved(arg)
this.status = 'fulfilled'
}
}
_Promise.prototype.reject = function(arg) {
if(this.status === 'pending') {
this.onRejected(arg)
this.status = 'rejected'
}
}
_Promise.prototype.then = function(onResolved) {
this.onResolved = onResolved
}
_Promise.prototype.reject = function(onRejected) {
this.onResolved = onRejected
}
複製程式碼
這就是一個最簡單的Promise實現方式,但是它還不支援鏈式呼叫。所以我們需要在then方法再返回一個Promise, 我們改造一下:
function _Promise(fn) {
this.status = 'pending';
this.onResolved = null;
this.onRejected = null;
this.childReject = null;
this.childResolve = null;
fn.call(this, this.resolve.bind(this), this.reject.bind(this))
}
_Promise.prototype.resolve = function(arg) {
if(this.status === 'pending') {
if(this.onResolved) {
var ret = this.onResolved(arg)
// 如果第二個then return的是一個使用者自定義的promise,我們稱為PromiseB,則需要把childResolve賦值給這個PromiseB的onResolved,交給這個PromiseB來執行
if(ret instanceof _Promise) {
ret.onResolved = this.childResolve
return
}
// 否則直接呼叫childResove確保第二個then的回撥執行
this.childResolve && this.childResolve()
}
this.status = 'fulfilled'
}
}
_Promise.prototype.reject = function(arg) {
if(this.status === 'pending') {
this.onRejected && this.onRejected(arg)
this.childReject && this.childReject()
this.status = 'rejected'
}
}
_Promise.prototype.then = function(onResolved) {
this.onResolved = onResolved
var that = this
// 定義一個promise,我們簡稱PromiseA,以便鏈式呼叫
return new _Promise(function(resolve, reject){
// 這裡需要確保resolve方法在第一個promise的resolve呼叫後呼叫,那麼需要把它儲存到第一個promise裡面
that.childResolve = resolve
that.childReject = reject
})
}
_Promise.prototype.reject = function(onRejected) {
this.onResolved = onRejected
}
// 例子
new _Promise((resolve) => {
setTimeout(() => {
resolve()
}, 2000)
}).then(() => {
console.log('promise success')
return new _promise((resolve) => {
setTimeout(() => {
resolve()
}, 1000)
})
}).then(() => {
console.log('promise2 success')
})
複製程式碼
這個實現,有三個Promise,第一個是首個Promise,第二個PormiseA即使如果then方法沒有返回使用者自定義的Promise的時候,我們方便鏈式呼叫的Promise,第三個PromiseB是then方法返回使用者自定義的Promise的時候,我們需要把第二個then的回撥交還給PromiseB執行。