1. 程式人生 > >JavaScript非同步之Promise

JavaScript非同步之Promise

傳統的JavaScript非同步通常基於回撥實現,但回撥方式有兩個重要的缺點:

  • 不便於除錯:由於回撥函式是基於事件佇列實現的,當回撥方法條用時,其外部呼叫函式並不在函式執行棧中,這給debug帶來了極大不便。來看下下面這個例子:

    function init(name) {
        test(name)
    }
    setTimeout(function A() {
        setTimeout(function() {
            init();
        }, 0);
    }, 0);

1.png

可以看到,setTimeout並未出現在異常堆疊中
  • 回撥地獄:在非同步程式設計中,通常會出現回撥巢狀的場景。一層層回撥相互巢狀,稱為回撥地獄。嚴重影響程式碼可讀性

Promise

由於傳統回撥的諸多缺點,Promise被提出以一種更友好的方式解決上述問題。Promise是什麼呢?簡單來說,Promise是一個封裝未來事件結果的可複用非同步任務管理機制。從這個定義中,我們可以看到Promise的幾個主要特點:

  • 非同步:Promise是用於描述未來事件的,未來事件什麼時候發生並不知曉,因而其必然是基於非同步實現的
  • 任務管理:當未來事件發生後,如何處理未來事件?未來事件成功如何處理?失敗又如何處理?所以Promise還涉及到任務管理
  • 可複用:一個未來事件可能有多個回撥處理,同時非同步任務也可能是多重巢狀的,即非同步任務回撥中還巢狀著另一個非同步任務。所以Promise
    必須是可複用的

術語

首先來看下Promise下幾個常用術語:

  • Promise:指一個擁有符合規範的then方法的物件
  • thenable:指一個定義了then方法的物件
  • resolve:改變一個promise物件從等待狀態到已完成或拒絕狀態,一旦改變,不可再改
  • reject reason:拒絕原因

另外,一個Promise中還有三個狀態:

  • pending:等待、初始狀態
  • fullfilled:已完成,未來事件操作成功
  • rejected:已拒絕,未來事件操作失敗

一個Promise物件的狀態變化只能有如下兩種:

pending ----->   fullfilled

pending ------>  rejected

THEN方法

Promise所提供的,用於訪問未來事件處理結果的方法:

Promise.then(onFulfilled, onRejected)
/*
* - 兩個引數均為可選,均有預設值,若不傳入,則會使用預設值;
* - 兩個引數必須是函式,否則會被忽略,使用預設函式;
* - onFulfilled: 在promise已完成後呼叫且僅呼叫一次該方法,該方法接受promise最終值作引數;
* - onRejected: 在promise被拒絕後呼叫且僅呼叫一次該方法,該方法接受promise拒絕原因作引數;
* - 兩個函式都是非同步事件的回撥,符合JavaScript事件迴圈處理流程
*/

Resolution

Promise的核心就是一個resolution的過程,即處理未來事件,並確定事件成功或失敗的條件,並在對應條件下執行onFullfilledonRejected(由then方法傳入)方法通知呼叫方。

接下來看一個Promise的例子:

let myPromise = new Promise((resolve, reject) =>{
    setTimeout(function(){
        console.log('resolve');
        resolve('success')
    }, 1000 * 3)
});

myPromise.then((msg) => {
    console.log("Yay!" + msg);
});

console.log("after execute promise");

對應的輸出為:

after execute promise
resolve
Yay!success

從輸出可以看出其非同步特性:Promise的例項化以及then方法都不是阻塞式函式,javascript依然繼續向下執行,所以最先輸出的便是after execute promise

(resolve, reject) =>{
    setTimeout(function(){
        console.log('resolve');
        resolve('success')
    }, 1000 * 3)
}

初始化Promise物件時傳入的處理函式是Promise的核心,如上述程式碼所示,在該Promise物件中設定一個3S的定時器。3S秒後,任務執行成功,所以通過呼叫resolve將成功資訊透出,同時resolve方法又會通過onResolved方法(即在then方法中傳入的處理函式)將該資訊透出給呼叫者。至此,一個完整的Promise流程執行完畢。其中resolve reject方法由Promise提供,使用者執行指定何時呼叫該方法即可。

接下來再來看一個例子:

let myPromise = new Promise((resolve, reject) =>{
    setTimeout(function(){
        console.log('resolve');
        resolve('success')
        reject('failed')
    }, 1000 * 3)
});

myPromise.then((msg) => {
    console.log(msg)
    console.log("Yay!" + msg);
    return 'first promise'
}, (reason) => {
    console.log('rejected:' + reason)
});

對應的輸出為:

resolve
success
Yay!success

可以看到,儘管同時呼叫了resolvereject,但只有resolve被執行了,這也再次驗證了Promise的狀態不可變性:即Promise的狀態一旦變為resolvedrejected便不會再改變。

接下來再改變下上述程式碼:

let myPromise = new Promise((resolve, reject) =>{
    setTimeout(function(){
        console.log('resolve');
        resolve('success')
        resolve('success')
    }, 1000 * 3)
});

myPromise.then((msg) => {
    console.log(msg)
    console.log("Yay!" + msg);
    return 'first promise'
}, (reason) => {
    console.log('rejected:' + reason)
});
resolve
success
Yay!success

可以看到,儘管呼叫了兩次resolve方法,但onResolve方法只執行了一次,即當promise物件的狀態一旦變為resolved或是rejected後,便不再執行resolvereject方法。

看完了上述的例子,我們重新來看下resolvereject方法:

Promise.resolve(x)

/*resolve方法返回一個已決議的Promsie物件:

若x是一個promise或thenable物件,則返回的promise物件狀態同x;
若x不是物件或函式,則返回的promise物件以該值為完成最終值;
否則,詳細過程依然按前文Promsies/A+規範中提到的規則進行。*/

Promsie.reject(reason)

/*返回一個使用傳入的原因拒絕的Promise物件。*/

Promise.prototype.then

看完了resolve方法和reject方法,接下來來看下then方法:

該方法為promsie新增完成或拒絕處理器,將返回一個新的promise,該新promise接受傳入的處理器呼叫後的返回值進行決議;若promise未被處理,如傳入的處理器不是函式,則新promise維持原來promise的狀態。

來看下下面這個例子:

var promise = new Promise((resolve, reject) => {
    setTimeout(function() {
        resolve('success');
    }, 10);
});
promise.then((msg) => {
  console.log('first messaeg: ' + msg);
}).then((msg) => {
    console.log('second messaeg: ' + msg);
});

其輸出結果為:

first messaeg: success
second messaeg: undefined

可以看到,第一個then方法成功接收到了resolve方法返回的結果,但第二個then方法接收到的卻是undefined。這是為什麼呢?then方法會返回一個promise物件,並且該新promise根據其傳入的回撥執行的返回值,進行決議,而函式未明確return返回值時,預設返回的是undefined,這也是上面例項第二個then方法的回撥接收undefined引數的原因。

所以接下來我們隊上次上述程式碼進行修改:

var promise = new Promise((resolve, reject) => {
    setTimeout(function() {
        resolve('success');
    }, 10);
});
promise.then((msg) => {
  console.log('first messaeg: ' + msg);
  return 'succss';
}).then((msg) => {
    console.log('second messaeg: ' + msg);
});

對應的輸出就變為:

first messaeg: success
second messaeg: success

Promise.prototype.catch

catch方法等同於then方法中的onRejected方法,為promise物件新增異常處理邏輯。

鏈式呼叫

正式由於then方法返回一個promise物件,所以可以基於Promise實現鏈式回撥呼叫:

var fourthPromise = new Promise((resolve, reject) => {
    setTimeout(()=>{
        console.log('first promise')
        resolve('first success');
    },1000);
}).then((msg) => {
    console.log(msg)
    return new Promise((resolve, reject) => {
        console.log('second promise')
        setTimeout(() => {resolve('second success')}, 1000)
    })
}).then((msg) => {
    console.log(msg)
    return new Promise((resolve, reject) => {
        console.log('third promise')
        setTimeout(() => {resolve('third success')}, 1000)
    })
});

fourthPromise.then((msg) => {
    console.log(msg)
})

對應的輸出為:

first promise
first success
second promise
second success
third promise
third success