為什麼ES6新增了Promise物件來處理非同步呼叫

newbeehh發表於2018-06-20

非同步呼叫是?:

首先我們得知道,什麼是非同步呼叫,而在前端中非同步呼叫最常見的場景無非就是處理ajax的請求響應了。

const client = new XMLHttpRequest();
client.open("GET","/");

console.log(1);
client.onlaod = function () {
    console.log(2);
};
client.send();
console.log(3);複製程式碼

要知道js是單執行緒的,因此是按順序執行該程式碼,理應列印的順序是1,2,3才對,但是實際輸出是1,3,2。可以看出ajax的響應處理被跳過了,放到了最後才被執行。

這就是非同步呼叫了:需要等待才能得結果的程式不會堵塞當前執行緒的執行,當該程式有結果後才會觸發當前執行緒去執行它的處理函式。

如何處理非同步呼叫:

說到這我們可能又要引入一個新問題了,就是在程式中如何處理非同步呼叫呢?

js中最常見的方法就是使用事件處理函式事件佇列

在ajax響應之前,我們先註冊事件處理函式(也叫回撥函式):

client.onlaod = function () {
    console.log(2);
};複製程式碼

在當ajax響應完畢後,處理響應結果的事件處理函式就會放到事件佇列中,等待當前執行緒去查詢(輪詢)並且執行它。

關於js的單執行緒和事件佇列的具體說明,同學們可以看看阮大的部落格

為什麼使用Promise:

總算到正文了,處理非同步呼叫看起來是不是很簡單,只需註冊相關的事件處理程式即可,那為啥又要搞出一個Promise類來呢?

const client = new XMLHttpRequest();
client.open("GET","/");
console.log(1);

client.onlaod = function () {
    console.log(2);
    
    const client2 = new XMLHttpRequest();
    client2.open("GET","/");
    client2.onlaod = function () {
        console.log(2.1);
    };
    client2.send();
};

client.send();
console.log(3);複製程式碼

有沒有遇到過這種需求,一個請求完畢後才能發起另一個請求。這樣的話另一個請求就要巢狀進前一個請求中了,一個兩個還好,但想象下要是有四五個呢,那程式碼將會變得非常難看和難以維護(回撥地獄)。而promise的出現正是要解決這個問題的。

Promise的原理和簡單使用:

Promise本身是ES6的一個內建類(注意它的開頭字母大寫),而它的作用就是儲存一個非同步操作,並提供相關的api去獲取其非同步操作的結果和設定多個非同步操作間的順序(注意非同步操作本身是無序的,只不過是Promise物件在內部實現了的巢狀和其他的相關方法使非同步操作達到有序的現象)。

首先初始化一個Promise物件,在建構函式中傳入我們涉及非同步操作函式即可。(這裡我們用setTImeOut來模擬非同步呼叫,方便檢視結果)。

const p1=new Promise(function (resolve) {
    console.log(1);
    setTimeout(function (n) {
        resolve(n);
    },3000,2);
    console.log(3);
});複製程式碼

可以看到,我們傳入涉及非同步操作的函式中還接收了一個函式引數,resolve

resolve函式會在非同步操作成功時執行,並且將非同步操作成功時的結果做為引數傳入,並且返回。如:

setTimeout(function (n) {
    resolve(n);
},3000,2);複製程式碼

三秒後,setTimeOut的回撥函式會執行resolve函式,傳入resolve函式的引數就是2,並且,resolve函式會將2返回。

是不是很簡單,就只需在原來的非同步事件處理函式(回撥函式)中呼叫resolve函式即可。並且傳遞其需要的結果做為變數。但是如何獲取其非同步的操作結果並進行進一步操作呢?

p1.then(function (res) {
    console.log(res);
});複製程式碼

是不是也很簡單,只需呼叫Promise物件的then函式(Promise的API),然後傳入一個處理函式,它的引數res就是resolve函式的返回值了(非同步操作的結果)。

忍住,是不是覺得我欺騙了你們,寫到這裡,我都覺得奇怪了,為什麼Promise這東西要寫兩次回撥函式,更加麻煩了,第一次是在非同步操作的事件處理函式中呼叫resolve函式,第二次是在then函式再次傳入一個回撥函式處理resolve的返回值。這簡直了。

穩住,其實這也是有道理的,resolve的作用主要有兩個:

第一個便是實現多個非同步操作的有序進行,解決回撥巢狀過度問題。

const p1=new Promise(function (resolve,reject) {
    setTimeout(function (n) {
        resolve(n);
    },3000,"p1");
});
const p2=new Promise(function (resolve,reject) {
    setTimeout(function () {
        console.log("p2");
        resolve(p1);
    },1000);
});

p2.then(function (resolve) {
    console.log(resolve);
});複製程式碼

看p2的resolve函式接收的是p1,當resolve函式接收一個Promise物件(p1)時,該resolve函式(p2的)會等待接收的Promise物件(p1)的resolve執行後再返回,並且返回值是該接收Promise物件(p1)的resolve的返回值。是不是有點繞,因此,p2等待1秒後就輸出了值“p2”,但是他的then函式確要等到resolve函式響應後才進行輸出,因此要等待3秒後才輸出了“p1”。

這樣是不是實現無巢狀情況下的非同步操作有序呼叫,(p2要等待p1)。突然覺得巢狀醜的就醜點吧,這特麼也太麻煩了。當然後來結合上ES6的await和async就好多了,這個同學們去阮大的ES6教程中看吧。

對了剛才說了resolve的作用有兩個,而另一個的話不過是把非同步操作的結果暴露出來了而已,這樣的話,非同步操作的回撥函式就可以在任何地點去定義了,不用像事件處理程式一樣一定要定義在非同步操作的前面。這又涉及到事件佇列多執行緒的的問題了,這個同學們實在不明白可以到時再問下我。

Promise的簡單使用就說到這兒了,要宣告下,這些都是簡化版的使用,比如Promise建構函式接收的回撥函式中其實還有個reject函式引數的,具體的同學們要去阮大的ES6教程中去學習。

後話:

第二篇在掘金的文章。



相關文章