JavaScript之淺析Promise

lzod發表於2019-01-07

1 背景

由於JavaScript是單執行緒語言,所以所有的網路請求、瀏覽器事件、自定義延時事件為了不阻塞主執行緒,都必須是非同步執行的,非同步執行可以用回撥函式來實現。

setTimeout(function () {
    console.log('success');        //1秒鐘後列印success
}, 1000);
複製程式碼

對於簡單的情況,回撥函式有著不錯的表現。

但是,如果情況變得複雜了呢?比如說一個延時事件必須等到另一個延時事件結束後才能執行。

setTimeout(function () {
    console.log('success');          //1秒鐘後列印success
    setTimeout(function () {
        console.log('again success');      //2秒鐘後列印again success
    }, 1000);
}, 1000);
複製程式碼

此時程式碼結構開始變得複雜起來。隨著複雜程度進一步的增加,回撥函式的巢狀層數會越來越多,最終形成回撥金字塔,使的程式碼變得難以閱讀。

Promise則正是為了解決這一問題而出現的。

2 建立一個Promise物件

Promise被用來處理非同步事件,當非同步事件執行完成後,會根據成功或失敗的結果,進行後續的邏輯操作。

首先,根據語法:

new Promise( function(resolve, reject) {...} /* executor */ );

我們來建立一個Promise物件

function executor(resolve,reject){
    //todo async event
}
var promise = new Promise(executor);
複製程式碼

executor函式是我們用來處理非同步事件的,它接收兩個引數resolve和reject。

當我們建立一個Promise物件(new Promise)的時候,Promise會立刻執行executor函式,並且把resolvereject兩個函式作為引數傳遞給executor函式。

在此之後,Promise會返回一個物件,我們用變數promise來接收這個物件。

3 使用Promise物件

3.1 resolve和reject


function executor(resolve,reject){
    setTimeout(function () {  
        if( isSuccess() ){           //isSuccess()是一個能夠隨機返回truefalse的函式,目的是為了模擬非同步事件是否執行成功
            resolve('success')
        }else{
            reject('run reject')
        }
    },1000)
}
var promise = new Promise(executor);
複製程式碼

我們知道,executor是我們用來處理非同步事件的,那麼無論這個非同步事件是否執行成功,總要發出一個訊號,告訴我們的promise物件非同步事件已經結束,可以進行下一步的操作了。

resolve和reject就是這個訊號的發起者了。

  • 如果非同步事件執行成功,那麼就呼叫resolve函式,並把成功的結果(例如字串success)作為引數傳遞進去。
  • 如果非同步事件執行失敗,那麼就呼叫reject函式,並把失敗的結果(例如字串fail)作為引數傳遞進去。

3.2 .then和.catch

function executor(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){           //isSuccess()是一個能夠隨機返回truefalse的函式,目的是為了模擬非同步事件是否執行成功
            resolve('success')
        }else{
            reject('fail')
        }
    },1000)
}

var promise = new Promise(executor);

promise.then(function(res){
    console.log('執行成功:',res)       //1秒鐘後,如果非同步事件執行成功,呼叫了resolve函式,則會列印執行成功:success
});
promise.catch(function(err){
    console.log('執行失敗:',err)      //1秒鐘後,如果非同步事件執行失敗,呼叫了reject函式,則會列印執行失敗:fail
});
複製程式碼

與resolve和reject對應的。.then和.catch就是訊號的接收者了。

  • 當executor函式呼叫resolve方法時,就會執行.then裡面的回撥函式,成功的結果(字串success)會作為引數傳遞給該回撥函式。
  • 當executor函式呼叫reject方法時,就會執行.catch裡面的回撥函式,失敗的結果(字串fail)會作為引數傳遞給該回撥函式。

.then和.catch裡面的回撥函式可以根據不同的非同步事件執行結果進行不同的後續操作。

3.3 .then和.catch回撥函式的特性

.then和.catch的回撥函式並不會立即執行,而是會被註冊到一個任務佇列裡,且同時具有以下3種特性:

  • 只有同時滿足非同步事件執行完成JavaScript主執行緒執行完畢這兩個條件,才會執行任務佇列裡的函式。
  • 即使非同步事件已經結束了,向任務佇列中註冊的回撥函式依然會被正確的執行。
  • 通過多次呼叫.then向任務佇列註冊多個回撥函式,它們會按照註冊的順序獨立的執行。(注意,.catch並不具備此特性)
console.log('main start');
let promise = new Promise(function(resolve){
    setTimeout(function () {
        resolve('success')
    },1000)
});
promise.then(function(res){
    console.log('執行成功:',res);
});
setTimeout(function () {
    promise.then(function(res){
        console.log('執行成功2:',res);
    });
},2000);
console.log('main end');

//執行順序如下:
//main start
//main end
//(1秒鐘後)執行成功:success
//(2秒鐘後,此時非同步事件已經結束了,但函式依然能被正確的執行)執行成功2:success
複製程式碼

3.4 小結

至此,一個Promise流程——從非同步事件發起(new Promise(executor)),到非同步事件結束(resolve或reject),再到根據不同的結果執行不同的操作(.then或.catch),就完整的結束了。

可以看出,與傳統的回撥函式相比,使用Promise結構會把非同步事件程式碼和處理結果程式碼分離開來,不僅利於閱讀,還有助於程式碼的複用。

在寫法上,由於:

  • 只使用一次的函式可以寫成匿名函式。所以可以把executor函式寫成匿名函式的形式。
  • new Promise和.then以及.catch都會返回一個Promise物件。所以可以把.then和.catch寫成鏈式呼叫的形式。

對Promise的應用也可以寫成以下形式。

new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('success')
        }else{
            reject('fail')
        }
    },1000)
}).then(function(res){
    console.log('執行成功:',res) 
}).catch(function(err){
    console.log('執行失敗:',err) 
});
複製程式碼

4 Promise方法

4.1 Promise.all()

有時候我們需要一次性的執行多個非同步事件,並且需要在這些非同步事件全部執行成功後再進行後續的操作。

這時候Promise.all()就派上了大用場。

var promise1 = new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('promise1 success')
        }else{
            reject('promise1 fail')
        }
    },1000)
})
var promise2 = new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('promise2 success')
        }else{
            reject('promise2 fail')
        }
    },2000)
})

var promiseList = [];
promiseList.push(promise1);
promiseList.push(promise2);
Promise.all(promiseList).then(resList=>{
    console.log('執行成功:',resList) //如果promise1和promise2都執行成功,那麼2秒後會列印 執行成功:['promise1 success','promise2 success'] 。除此之外,都會執行.catch的回撥函式。
}).catch(err=>{
    console.log('執行失敗:',err)  //根據失敗的來源,列印結果有兩個 執行失敗:promise1 fail 或者 執行失敗:promise2 fail
});
複製程式碼

Promise.all()接收一個陣列,該陣列的元素是promise物件。Promise.all()會等待陣列裡面所有的promise都執行成功,或者第一個執行失敗。

  • 當所有的promise都執行成功時,就會呼叫.then的回撥函式,並把所有的成功結果按照順序放到一個陣列裡,作為引數傳遞給該回撥函式。也就是說,promise在陣列中的順序與成功結果在陣列中的順序是一致的。
  • 當第一個promise執行失敗時,就會呼叫.catch的回撥函式,並把該promise的失敗結果作為引數傳遞給該回撥函式。

4.2 Promise.race()

有時候我們需要一次性的執行多個非同步事件,並且當第一個非同步事件執行成功或失敗就進行後續的操作,而不關心剩餘非同步事件的執行結果。

這時候就可以使用Promise.race()來解決這一類問題。

var promise1 = new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('promise1 success')
        }else{
            reject('promise1 fail')
        }
    },1000)
})
var promise2 = new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('promise2 success')
        }else{
            reject('promise2 fail')
        }
    },2000)
})

var promiseList = [];
promiseList.push(promise1);
promiseList.push(promise2);
Promise.race(promiseList).then(res=>{
    console.log('執行成功:',res) //當第一個promise執行成功時,列印該promise成功結果   執行成功:promise1 success 或者 執行成功:promise2 success
}).catch(err=>{
    console.log('執行失敗:',err)  //當第一個promise執行失敗時,列印該promise失敗結果   執行失敗:promise1 fail 或者 執行失敗:promise2 fail
});
複製程式碼

Promise.all()接收一個陣列,該陣列的元素是promise物件。Promise.race()會等待陣列裡面的第一個promise執行成功或者執行失敗。

  • 當第一個promise執行成功時,就會呼叫.then的回撥函式,並把該promise的成功結果作為引數傳遞給該回撥函式。
  • 當第一個promise執行失敗時,就會呼叫.catch的回撥函式,並把該promise的失敗結果作為引數傳遞給該回撥函式。

4.3 Promise.resolve()

Promise.resolve()會直接返回一個執行成功的Promise物件。

Promise.resolve('success').then(function(res) {
  console.log('執行成功:',res);    //直接列印  執行成功:success
});
複製程式碼

我們一般很少用到這個方法,但是這個方法解釋了為什麼.then和.catch能夠鏈式呼叫。

上文說到過:

new Promise和.then以及.catch都會返回一個Promise物件。所以可以把.then和.catch寫成鏈式呼叫的形式。

new Promise(function(resolve,reject){
    setTimeout(function () {
        if( isSuccess() ){
            resolve('success')
        }else{
            reject('fail')
        }
    },1000)
}).then(function(res){
    console.log('執行成功:',res) 
}).catch(function(err){
    console.log('執行失敗:',err) 
});
複製程式碼

首先,Promise作為一個類,new Promise的時候返回一個物件,這沒問題(詳情請參見ES6 class)。

但是.then和.catch為什麼也會返回一個promise物件呢?

這是因為在JavaScript中,函式都是有返回值的,如果沒有返回值,那麼函式會隱式的(自動的)返回一個undefined。

而.then和.catch又會把函式的返回值隱式的(自動的)用Promise.resolve()包裝一下。

所以,.then和.catch就成功的返回了一個promise物件,也就能實現鏈式呼叫的書寫結構了。

而這個鏈式呼叫的特性同時解決了回撥函式的回撥金字塔問題。

new Promise(function(resolve){
    setTimeout(function () {
        resolve('success')
    },1000)
}).then(function(res){
    console.log(res);         //1秒鐘後列印 success
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve('again success')
        },1000)
    })
}).then(function(res){
    console.log(res);        //2秒鐘後列印 again success
});
複製程式碼

對比多層巢狀的函式回撥結構,鏈式呼叫結構清晰,更利於閱讀和維護。

4.4 Promise.reject()

Promise.reject()會直接返回一個執行失敗的Promise物件。

Promise.reject('fail').catch(function (err) {
    console.log('執行失敗:',err) //直接列印 執行失敗:fail
});
複製程式碼

這個方法也很少用到。但值得一提的是,.catch除了能響應reject之外,也能響應在promise流程中的異常丟擲事件。

Promise.reject('fail').catch(function (err) {
    console.log('執行失敗:',err); //列印結果 執行失敗:fail
    throw 'throw fail'
}).catch(function (err) {
    console.log('執行失敗:',err); //列印結果 執行失敗:again fail
});
複製程式碼

5 總結

Promise作為處理非同步事件的解決方案。

  • 很好的把非同步事件程式碼和處理結果程式碼分離開來。
  • 提供了all和race方法來解決一次性執行多個非同步事件這一類問題。
  • 其鏈式呼叫的特性很好的解決了函式回撥的問題。

靈活的運用Promise將會為我們的開發工作提供很大的便利。

相關文章