Javascript — Promise

boxZhang發表於2019-03-03

什麼是Promise

Promise是抽象非同步處理物件以及對其進行各種操作的元件。

Promise真的很重要很重要,一定要好好掌握。

// Promise 例項:
var promise = new Promise((resolve,reject) => {
    if(true) { resolve(100) };
    if(false) { reject('error') };
});
//使用
promise.then(value => {
    console.log(value);		//100
}).catch(error => {
    console.error(error);
});
複製程式碼

回撥函式

在解釋Promise之前,先來回顧一下什麼是回撥函式。

回撥函式,也被稱作高階函式。

函式A作為引數(函式引用)傳遞到另一個函式B中,並且這個函式B執行函式A。我們就說函式A叫做回撥函式。如果沒有名稱(函式表示式),就叫做匿名回撥函式。

注意:回撥函式不是立即就執行。它是在包含的函式體中指定的地方“回頭呼叫”。

網上有一個通俗易懂的例子幫助理解回撥函式:

你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。在這個例子裡,你的電話號碼就叫回撥函式,你把電話留給店員就叫登記回撥函式,店裡後來有貨了叫做觸發了回撥關聯的事件,店員給你打電話叫做呼叫回撥函式,你到店裡去取貨叫做響應回撥事件。

//回撥函式舉例1
$("#btn").click(() => {
	alert("點選後才出現");
});
//回撥函式舉例2
function runAsyncCallback(callback){
    setTimeout(() => {
        console.log('執行完成');
        callback('資料');
    }, 2000);
}
runAsync(data=>{
    console.log(data);	//2秒後先輸出:執行完成,再輸出:資料
});
複製程式碼

認識了回撥函式,接下來的內容會幫助我們理解為什麼需要Promise。

為什麼需要Promise

有非常多的應用場景我們不能立即知道應該如何繼續往下執行。例如很重要的ajax請求的場景。通俗來說,由於網速的不同,可能你得到返回值的時間也是不同的,這個時候我們就需要等待,結果出來了之後才知道怎麼樣繼續下去,例如下方的回撥函式案例:

// 需求:當一個ajax結束後,得到的值,需要作為另外一個ajax的引數被使用(即該引數得從上一個ajax請求中獲取)
var url = 'XXXXXX';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);
        // 虛擬碼
        var url2 = 'XXXXXX' + result.someParams;
        var XHR2 = new XMLHttpRequest();
        XHR2.open('GET', url2, true);
        XHR2.send();
        XHR2.onreadystatechange = function() {
            ...
        }
    }
}
複製程式碼

當上述需求中出現第三個ajax(甚至更多)仍然依賴上一個請求的時候,程式碼就會變成一場災難。也就是我們常說的回撥地獄

這時,我們可能會希望:

  1. 讓程式碼變得更具有可讀性和可維護性
  2. 將請求和資料處理明確的區分開

這時Promise就要閃亮登場了,Promise中有一個強大的then方法,可以解決剛剛遇到的回撥地獄問題,並且讓程式碼更優雅。

下面我們就一起來學習一下Promise,看一看它的強大之處。

Promise 的API

1、constructor (建構函式屬性)

Promise本身也是一個建構函式,需要通過這個建構函式建立一個新的promise物件作為介面,使用new來呼叫Promise的構造器來進行例項化,所以這個例項化出來的新物件:具有constructor屬性,並且指標指向他的建構函式Promise。

var promise = new Promise((resolve, reject) => {
    // 此處程式碼會立即執行
    // 當呼叫棧內容處理結束後,再通過promise.then()方法呼叫resolve 或 reject返回的資料
});
複製程式碼

2、Instance Method (例項方法)

promise.then()

Promise物件中的promise.then(resolve,reject) 例項方法,可以接收建構函式中處理的狀態變化,並分別對應執行。

promise.then(onFulfilled, onRejected)
複製程式碼

then方法有2個引數(都是可選引數):

  • resolve 成功時onFulfilled 會被呼叫
  • reject 失敗時onRejected 會被呼叫

promise.then 成功和失敗時都可以使用,並且then方法的執行結果也會返回一個Promise物件

promise.catch()

另外在只想對異常進行處理時可以採用 promise.then(undefined, onRejected) 這種方式,只指定reject時的回撥函式即可。 不過這種情況下 promise.catch(onRejected) 應該是個更好的選擇。

promise.catch(onRejected)
複製程式碼

注意:在IE8及以下版本,使用 promise.catch() 的程式碼,會出現 identifier not found 的語法錯誤。(因為 catch 是ECMAScript的 保留字 (Reserved Word)有關。在ECMAScript 3中保留字是不能作為物件的屬性名使用的。)

解決辦法:不單純的使用 catch ,而是使用 then 來避免這個問題。

--------------------------------------------------

//then和catch方法 舉例
function asyncFunction(value) {
    var p = new Promise((resolve, reject) => {
        if(typeof(value) == 'number'){
            resolve("數字");
        }else {
            reject("我不是數字");
        }
    });
    return p;
}

// 寫法1:同時使用then和catch方法
asyncFunction('123').then(value => {	
    console.log(value);   
}).catch(error => {
    console.log(error);
});
//執行結果:數字

// 寫法2:只使用 then方法,不使用catch 方法
// asyncFunction('abc').then(value => {	
//     console.log(value);   
// },(error) => {
//     console.log(error);
// });
//執行結果:我不是數字

複製程式碼

3、Static Method (靜態方法)

Promise 這樣的全域性物件還擁有一些靜態方法。

Promise.all()

Promise.resolve()

……

Promise 的狀態 (Fulfilled、Rejected、Pending)

Promise的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫。

new Promise 例項化的promise物件有以下三個狀態。

  • "unresolved" - Pending | 既不是resolve也不是reject的狀態。等待中,或者進行中,表示Promise剛建立,還沒有得到結果時的狀態

  • "has-resolution" - Fulfilled | resolve(成功)時。此時會呼叫 onFulfilled

  • "has-rejection" - Rejected | reject(失敗)時。此時會呼叫 onRejected

Javascript — Promise

關於上面這三種狀態的讀法,其中 左側為在 ES6 Promises 規範中定義的術語, 而右側則是在 Promises/A+ 中描述狀態的術語。

promise物件的狀態,從Pending轉換為FulfilledRejected之後, 這個promise物件的狀態就不會再發生任何變化。

當promise的物件狀態發生變化時,用.then 來定義只會被呼叫一次的函式。

Promise的使用

1、建立Promise物件

前面很多次強調,Promise本身就是一個建構函式,所以可以通過new建立新的Promise物件:

var p = new Promise((resolve, reject) => {
    //做一些非同步操作
    setTimeout(() => {
        console.log('執行完成');
        resolve('我的資料');
    }, 0);
    console.log("我先執行")
});
//先輸出:我先執行
//1秒之後輸出: 執行完成
複製程式碼

我們執行了一個非同步操作,也就是setTimeout,1秒後,輸出“執行完成”,並且呼叫resolve方法。但是隻是new了一個Promise物件,並沒有呼叫它,我們傳進去的函式就已經執行了。為了避免這個現象產生, 所以我們用Promise的時候一般是包在一個函式中,需要的時候去執行這個函式。

如果你對執行的先後順序還不理解,請參見事件的迴圈機制(Event loop)

非同步任務:指不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有等主執行緒任務執行完畢,"任務佇列"開始通知主執行緒,請求執行任務,該任務才會進入主執行緒執行。

也可以理解為可以改變程式正常執行順序的操作就可以看成是非同步操作。例如setTimeout和setInterval函式

同步任務:指在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;

參考文章:徹底理解同步、非同步和事件迴圈(Event Loop)

2、封裝Promise物件

function asyncFunction(num) {
    var p = new Promise((resolve, reject) => {	//建立一個Promise的新物件p
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    });
    p.then(function() {	//第一個function是resolve對應的引數
        console.log('數字');
    }, function() {		//第二個function是reject對應的引數
        console.log('我不是數字');
    })
    return p;	//此處返回物件p
}

//執行這個函式我們得到了一個Promise構造出來的物件p,所以p.__proto__ === Promise.prototype,即p的指標指向了建構函式Promise,因此asyncFunction()能夠使用Promise的屬性和方法

//此種寫法可以多次呼叫asyncFunction這個方法
asyncFunction('hahha');	//我不是數字
asyncFunction(1234);	//數字

複製程式碼

我們剛剛講到,then方法的執行結果也會返回一個Promise物件,得到一個結果。因此我們可以進行then的鏈式執行,接收上一個then返回回來的資料並繼續執行,這也是解決回撥地獄的主要方式。

3、Promise的鏈式操作和資料傳遞

下面我們就來看看如何確認then和catch兩個方法返回的到底是不是新的promise物件。

var aPromise = new Promise(resolve => {
    resolve(100);
});
var thenPromise = aPromise.then(value => {
    console.log(value);
});
var catchPromise = thenPromise.catch(error => {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
複製程式碼

=== 是嚴格相等比較運算子,我們可以看出這三個物件都是互不相同的,這也就證明了 thencatch 都返回了和呼叫者不同的promise物件。我們通過下面這個例子進一步來理解:

// 1: 對同一個promise物件同時呼叫 `then` 方法
var aPromise = new Promise(resolve => {
    resolve(100);
});
aPromise.then(value => {
    return value * 2;
});
aPromise.then(value => {
    return value * 2;
});
aPromise.then(value => {
    console.log("1: " + value); // 1: 100
})

// vs

// 2: 對 `then` 進行 promise chain 方式進行呼叫
var bPromise = new Promise(resolve => {
    resolve(100);
});
bPromise.then(value => {
    return value * 2;
}).then(value => {
    return value * 2;
}).then(value => {
    console.log("2: " + value); // 2: 400
});
複製程式碼

第1種寫法中並沒有使用promise的方法鏈方式,這在Promise中是應該極力避免的寫法。這種寫法中的 then 呼叫幾乎是在同時開始執行的,而且傳給每個 then 方法的 value 值都是 100

第2中寫法則採用了方法鏈的方式將多個 then 方法呼叫串連在了一起,各函式也會嚴格按照 resolve → then → then → then 的順序執行,並且傳給每個 then 方法的 value 的值都是前一個promise物件通過 return 返回的值,實現了Promise的資料傳遞

4、通過Promise封裝ajax 解決回撥地獄問題

我們在開篇,通過一個ajax的例子,引出了回撥地獄的概念,強調了通過回撥函式方式解決 多級請求都依賴於上一級資料時 所引發的問題。下面我們通過剛剛學習過的Promise對上面的ajax資料依賴的案例進行重寫:

var url = 'XXXXX';

// 封裝一個get請求的方法
function getJSON(url) {
    return new Promise((resolve, reject) => {
        var XHR = new XMLHttpRequest();
        XHR.open('GET', url, true);
        XHR.send();

        XHR.onreadystatechange = function() {
            if (XHR.readyState == 4) {
                if (XHR.status == 200) {
                    try {
                        var response = JSON.parse(XHR.responseText);
                        resolve(response);
                    } catch (e) {
                        reject(e);
                    }
                } else {
                    reject(new Error(XHR.statusText));
                }
            }
        }
    })
}

getJSON(url)
    .then(resp => {
        console.log(resp);
        return url2 = 'http:xxx.yyy.com/zzz?ddd=' + resp;
    })
    .then(resp => {
        console.log(resp);
        return url3 = 'http:xxx.yyy.com/zzz?ddd=' + resp;
    });

複製程式碼

new Promise寫法的快捷方式

1、Promise.resolve

new Promise(resolve => {
    resolve(100);
});
// 等價於
Promise.resolve(100);	//Promise.resolve(100); 可以認為是上述程式碼的語法糖。

// 使用方法
Promise.resolve(100).then(value => {
    console.log(value);
});

複製程式碼

--------------------------------------------------

另:`Promise.resolve` 方法另一個作用就是將 [thenable](http://liubin.org/promises-book/#Thenable) 物件轉換為promise物件。

[ES6 Promises](http://liubin.org/promises-book/#es6-promises)裡提到了[Thenable](http://liubin.org/promises-book/#Thenable)這個概念,簡單來說它就是一個非常類似promise的東西。

就像我們有時稱具有 `.length` 方法的非陣列物件為Array like(類陣列)一樣,thenable指的是一個具有 `.then` 方法的物件。

將thenable物件轉換promise物件
複製程式碼
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise物件
promise.then(function(value){
   console.log(value);
});
複製程式碼

--------------------------------------------------

2、Promise.reject

new Promise((resolve,reject) => {
    reject(new Error("出錯了"));
});
// 等價於
 Promise.reject(new Error("出錯了"));	// Promise.reject(new Error("出錯了")) 就是上述程式碼的語法糖。

// 使用方法
Promise.reject(new Error("BOOM!")).catch(error => {
    console.error(error);
});
複製程式碼

Promise.all()

Promise.all 接收一個 promise物件的陣列作為引數,當這個陣列裡的所有promise物件全部變為resolve或reject狀態的時候,它才會去呼叫 .then 方法。

也就是說:Promise的all方法提供了並行執行非同步操作的能力,並且在所有非同步操作執行完後才執行回撥。

// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
var startDate = Date.now();
// 所有promise變為resolve後程式退出
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(values => {
    console.log(Date.now() - startDate + 'ms');
    // 約128ms
    console.log(values);    // [1,32,64,128]
});
複製程式碼

這說明timerPromisefy 會每隔1, 32, 64, 128 ms都會有一個promise發生 resolve 行為,返回一個promise物件,狀態為FulFilled,其狀態值為傳給 timerPromisefy 的引數,並且all會把所有非同步操作的結果放進一個陣列中傳給then。

從上述結果可以看出,傳遞給 Promise.all 的promise並不是一個個的順序執行的,而是同時開始、並行執行的。

Promise.race()

all方法的效果實際上是「誰跑的慢,以誰為準執行回撥」,那麼相對的就有另一個方法「誰跑的快,以誰為準執行回撥」,這就是race方法,這個詞本來就是賽跑的意思。race的用法與all一樣,接收一個promise物件陣列為引數。

Promise.all 在接收到的所有的物件promise都變為 FulFilled 或者 Rejected 狀態之後才會繼續進行後面的處理, 與之相對的是 Promise.race 只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理。

// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
// 任何一個promise變為resolve或reject 的話程式就停止執行
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (value) {
    console.log(value);    // => 1
});
複製程式碼

上面的程式碼建立了4個promise物件,這些promise物件會分別在1ms,32ms,64ms和128ms後變為確定狀態,即FulFilled,並且在第一個變為確定狀態的1ms後, .then 註冊的回撥函式就會被呼叫,這時候確定狀態的promise物件會呼叫 resolve(1) 因此傳遞給 value 的值也是1,控制檯上會列印出1來。

小練習

下面內容的輸出結果應該是啥?

function taskA() {
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);
複製程式碼

溫馨提示:我們沒有為 then 方法指定第二個引數(onRejected)

參考

JavaScript Promise迷你書

透徹掌握Promise的使用

詳解事件迴圈機制

併發模型與事件迴圈


如若發現文中紕漏請留言,歡迎大家糾錯,我們一起成長。

相關文章