Javascript非同步編程的方法 - Promise
javascript
Lastmod: 2019-12-13

在上一篇介紹 PubSub 的方法後,發現該方法不適合處理一次性事件,而Promise就是用來解決該問題的手法。那什麼是Promise呢?,他是一種非同步操作的最終結果,你也可以把想成是未來的物件但是現在還不可用,在未來他會有多種狀況,可能是成功又或是失敗,當未來發生成功時他就執行成功的 callBack fucntion,但它失敗`時就執行失敗的callback function。

Promise/A+ 規範

上述說的promise只能說是一種概念,然後有很多人會針對它進行實作,但是因為都沒個規範,所以每個人做出來的promise都不太一樣,因此Kris Zyp提出了 CommonJs 的 promises/A 規範,符合條件如下。

規範 1 : Promise狀態

一個Promise必須要處於以下三種狀態。pending, fulfilled, or rejected

  • pending : 當為Pending 狀態時,可以轉換至f fulfilled 或 rejected。
  • fulfilled : 通常是代表成功。
  • rejected : 通常代表失敗。

規範 2 : Promise必須要 Then

一個Promise必須提供Then方法,並且接受兩個參數,並且第一個參數onFulfilledfulfilled執行後調用,而onRejectedrejected後調用。

	promise.then(onFulfilled, onRejected)

其它詳細的規範其參考下面的連結。。

hpromises-spec

Jquery 的 Promise 實現

Jquery1.5之後,我們常用的$.ajax$.get$.getJson等這些ajax函數全部都會返回promise,下面給個例子來看看差別。

versin 1.4

	$.get('/getData', { 
	   success: onSuccess, 
	   failure: onFailure
	});
version 1.5

	var promise = $.get('/getData');
	promise.done(onSuccess);
	promise.fail(onFailure);

這種改變的好處在於封裝,你可以將複雜非同步處理輕鬆的模式化,例如希望任務1與任務2完成時在執行任務3,或是任務1執行完在執行任務2這種複雜的非同步任務都可以用promise來解決。

Jquery的 Promise 與 Deferred

我們要如何把一個函數變成Promise對象?首先我們需要產生Deferred,什麼是DeferredDeferred嚴格來說為Promise的超集合,它比Promise多了一項關鍵特性,那就是他可以直接的觸發,單純的Promise實例只能允許增加callBack function,必須由其它東西來觸發這些callback function

如下程式碼,如果我們要將deferredTest變成Promise,我們需要一個deferred,然後在執行成功時,執行deferred.resolve(),並且失敗時執行deferred.reject(),然後它們就會觸發相對應的callback function

	function deferredTest(){
		var deferred = new $.Deferred();
		$.ajax('.getData',{
			success:function(){
				deferred.resolve();
			},
			fail:function(){
				deferred.reject();
			}	
		});
		return deferred;
	}

根據範例deferred.resolve()就會觸發onSuccess,而deferred.reject()則會觸發onFailure

	var promise = deferredTest();
	promise.done(onSuccess);
	promise.fail(onFailure);

向 callback function 傳遞參數

通常在實務上,ajax很常去後端取得資料,因此通常都有回傳資料,這時可以提供資料給callback function,如下程式碼。

	function getData(){
		var deferred = new $.Deferred();
		$.ajax('.getData',{
			success:function(data){
				deferred.resolve(data);
			},
			fail:function(errorMessage){
				deferred.reject(errorMessage);
			}	
		});
		return deferred;
	}

下面程式碼的data就是ajax回傳的資料。

	var promise = getData();
	promise.done(function onSuccess(data){
		console.log(data);
	});

ES6 的 Promise

在 es6 時它有提供內建的 promise ,它有提供以下幾種方法 :

Promise.all

回傳一個 promise

這種用方法,可以指定一系列的 promise 任務,不依照順序來執行,當其中一個任務失敗時,它就會 rejects ,而當所以任務都完成後,它就會 resolve。

下面程式碼中,為我們使用promise.all的結果,我們傳入的任務順序是3 (3秒) -> 2 (2秒) -> 1(1秒)

function handler(url, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(url);
            resolve(url);
        } , time)
    });
};

function process(objs){
    let tasks = [];
    objs.forEach((obj) => {
        tasks.push(handler(obj,obj*1000));
    });
    return Promise.all(tasks);
}


var test = [3, 2, 1];
process(test).then(() => {
    console.log("All success")
});

然後我們執行的結果如預期,不會依順序來執行,輸入的結果是依時間花時的順序。

1
2
3
All success.

Promise.race

回傳一個 promise

它是傳入一系列的 promise 任務,只要其中一個任務失敗或成都,就會直接 resolve 或 reject。但其它的任務還是會繼續執行。

function handler(url, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(url);
            resolve(url);
        } , time)
    });
};

function process(objs){
    let tasks = [];
    objs.forEach((obj) => {
        tasks.push(handler(obj,obj*1000));
    });
    return Promise.race(tasks);
}

var test = [3, 2, 1];

process(test).then(() => {
    console.log("All success")
});

執行結果。

1 
All success
2
3

Promise.resolve 與 Promise.reject

回傳一個 promise

這兩個方法都會回傳一個 promise ,差別在於,一個已經resolve,另一個已reject,簡單的使用範例如下。

Promise.resolve(2).then((data) => {
   console.log(data);
})

// console.log 結果為 2

而 reject 的用法如下 :

Promise.reject("error message").catch((err) => {
   console.log(err);
})

// console.log 結果為 "error message"

我們如果要依序執行要如何做 ?

我們這邊直接寫個最簡單的方法,這邊我們需要一個 promise.resolve 來產生一個已經 resolve 後的 promise ,然後在每一次的 result.then 裡面回傳 handler 回傳結的 promise ,這樣就能產生像 promise chain 的東西。

function handler(url, time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(url);
            resolve(url);
        } , time)
    });
};

function process(objs) {
    let result = Promise.resolve();
    objs.forEach((obj) => {
        result = result.then(() =>{
        return handler(obj,obj*1000);
        });
    })
    return result;
}

var test = [3, 2, 1];

process(test).then(() => {
    console.log("All success")
});

執行結果

3
2
1
All success

不過這種寫法有個缺點,那就是如果有很多任務時,他會需要先全部註冊然後再來執行,建議使用像是buldbird 這種 promise libary 來處理會比較優質點兒 ~

參考資料

comments powered by Disqus