ES6的promise物件研究

龍恩0707發表於2015-08-30

ES6的promise物件研究

什麼叫promise?

     Promise物件可以理解為一次執行的非同步操作,使用promise物件之後可以使用一種鏈式呼叫的方式來組織程式碼;讓程式碼更加的直觀。

那我們為什麼要使用promise?

比如我們在工作中經常會碰到這麼一個需求,比如我使用ajax發一個A請求後,成功後拿到資料,我們需要把資料傳給B請求;那麼我們需要如下編寫程式碼:

$.ajax({
      url: '',
      dataType:'json',
      success: function(data) {
          // 獲取data資料 傳給下一個請求
          var id = data.id;
          $.ajax({
              url:'',
              data:{"id":id},
              success:function(){
                    // .....
              }
          });
      }
});

如上程式碼;上面的程式碼有如下幾點缺點:

  1. 後一個請求需要依賴於前一個請求成功後,將資料往下傳遞,會導致多個ajax請求巢狀的情況,程式碼不夠直觀。
  2. 如果前後兩個請求不需要傳遞引數的情況下,那麼後一個請求也需要前一個請求成功後再執行下一步操作,這種情況下,那麼也需要如上編寫程式碼,導致程式碼不夠直觀。

因此針對這種情況下,我們可以使用promise物件來解決上面的問題了~

下面我們下來看看目前支援promise瀏覽器的情況如下:

支援的情況如上顯示;

如果我們想使用promise物件的話,我們可以使用中的promise物件,這個稍後慢慢來講解jquery中的promise物件;

我們先來看看ES6中的promise物件吧!

如何建立promise物件?

要想建立promise物件,可以使用new來呼叫promise的構造器來進行例項化。

如下程式碼:

var promise = new Promise(function(resolve,reject){
    // 非同步處理
    // 成功呼叫resolve 往下傳遞引數 且只接受一個引數
    // 失敗呼叫reject  往下傳遞引數 且只接受一個引數
});

對通過new 生成的promise物件為了設定其值在resolve(成功) / reject(失敗) 時呼叫的回撥函式,可以使用promise.then()例項方法。

如下程式碼:

promise.then(onFulfilled, onRejected);

resolve(成功) 時 呼叫onFulfilled 方法,reject(失敗) 時 呼叫onRejected方法;

Promise.then 成功和失敗時都可以使用,如果出現異常的情況下可以採用

promise.then(undefined,onRejected) 這種方式,只指定onRejected回撥函式即可,不過針對這種情況下我們有更好的選擇是使用catch這個方法;程式碼如下:

promise.catch(onRejected);

上面囉嗦了這麼多,我們來分別來學習相關的promise物件中的方法知識點吧!

理解Promise.resolve

一般情況下我們都會使用new Promise()來建立promise物件,但是我們也可以使用promise.resolve 和 promise.reject這兩個方法;

Promise.resolve(value)的返回值也是一個promise物件,我們可以對返回值進行.then呼叫;如下程式碼:

Promise.resolve(11).then(function(value){
    console.log(value); // 列印出11
});

resolve(11)程式碼中,會讓promise物件進入確定(resolve狀態),並將引數11傳遞給後面的then所指定的onFulfilled 函式;

我們上面說過建立promise物件,可以使用new Promise的形式建立物件,但是我們這邊也可以使用Promise.resolve(value)的形式建立promise物件;

理解Promise.reject

Promise.reject 也是new Promise的快捷形式,也建立一個promise物件,比如如下程式碼:

Promise.reject(new Error("我錯了,請原諒俺!!"));

就是下面的程式碼new Promise的簡單形式:

new Promise(function(resolve,reject){
     reject(new Error("我錯了,請原諒俺!!"));
});

下面我們來綜合看看使用resolve方法和reject方法的demo如下:

function testPromise(ready) {
    return new Promise(function(resolve,reject){
        if(ready) {
            resolve("hello world");
        }else {
            reject("No thanks");
        }
    });
};
// 方法呼叫
testPromise(true).then(function(msg){
    console.log(msg);
},function(error){
    console.log(error);
});

上面的程式碼的含義是給testPromise方法傳遞一個引數,返回一個promise物件,如果為true的話,那麼呼叫promise物件中的resolve()方法,並且把其中的引數傳遞給後面的then第一個函式內,因此列印出 “hello world”, 如果為false的話,會呼叫promise物件中的reject()方法,則會進入then的第二個函式內,會列印No thanks;

理解Promise非同步呼叫的操作

如下程式碼:

var promise = new Promise(function(resolve){
    console.log(1);
    resolve(3);
});
promise.then(function(value){
    console.log(value);
});
console.log(2);

上面的程式碼輸出我們可以看到,分別為 1,2,3; 首先程式碼從上往下執行,首先輸出1,然後呼叫resolve(3)這個方法,這時候promise物件變為確定狀態,即呼叫onFulFilled這個方法,從上面瞭解到,resolve(成功) 時 呼叫onFulfilled 方法,Promise.then 成功和失敗時都可以使用,因此第一個函式是成功呼叫的,但是Promise物件是以非同步方式呼叫的,所以先執行console.log(2),輸出的是2,然後輸出的是3;

理解是同步呼叫還是非同步呼叫

如下程式碼:

function ready(fn){
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        fn();
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
ready(function(){
    console.log("DOM Load Success");
});
console.log("我是同步輸出的");

如上程式碼;如果在呼叫ready()方法之前DOM已經載入完成的話,就會對回撥函式進行同步呼叫,先輸出DOM Load Success 後輸出 我是同步輸出的 文案;如果在呼叫ready()方法之前DOM為未載入完成的話,那麼程式碼先會執行 window.addEventListener('DOMContentLoaded', fn);

就會非同步呼叫該函式,那麼就會先輸出 “我是同步輸出的”,後輸出”DOM Load Success”;

為了解決上面的同步或者非同步混亂的問題,我們現在可以使用promise物件使用非同步的方式來解決;如下程式碼

function readyPromise(){
    return new Promise(function(resolve,reject){
        var readyState = document.readyState;
        if (readyState === 'interactive' || readyState === 'complete') {
            resolve();
        } else {
            window.addEventListener('DOMContentLoaded', resolve);
        }
    });
}
readyPromise().then(function(){
    console.log("DOM Load Success");
});
console.log("我是同步載入的,先執行我");

輸出如下:先輸出"我是同步載入的,先執行我" 後輸出 "DOM Load Success"。因為promise物件是非同步載入的。

理解promise的三種狀態

Promise 物件有三種狀態:

  Resolve 可以理解為成功的狀態;

  Rejected 可以理解為失敗的狀態;

  Pending既不是Resolve也不是Rejected狀態;可以理解為Promise物件例項建立時候的初始狀態;

比如Promise物件中的resolve方法就是呼叫then物件的第一個函式,也就是成功的狀態;而reject方法就是呼叫then物件的第二個函式,也就是失敗的狀態;

理解then()

上面的程式碼,比如如下這樣的程式碼就是then的列子;程式碼如下:

function testPromise(ready) {
    return new Promise(function(resolve,reject){
        if(ready) {
            resolve("hello world");
        }else {
            reject("No thanks");
        }
    });
};
// 方法呼叫
testPromise(true).then(function(msg){
    console.log(msg);
},function(error){
    console.log(error);
});

上面的程式碼就是利用了 then(onFulfilled,onRejected)方法來執行的,第一個方法就是成功狀態的標誌,第二個方法是失敗的狀態標誌;

當然在多個任務的情況下then方法同樣可以使用;比如上面的程式碼改成如下:

function testPromise(ready) {
    return new Promise(function(resolve,reject){
        if(ready) {
            resolve("hello world");
        }else {
            reject("No thanks");
        }
    });
};
// 方法呼叫
testPromise(true).then(function(msg){
    console.log(msg);
}).then(testPromise2)
  .then(testPromise3);
function testPromise2(){
    console.log(2);
}
function testPromise3(){
    console.log(3);
}

輸出如下:hello world ,2,3

上面的程式碼是then的鏈式呼叫方式,輸出是按順序輸出的 分別為 hello world , 2,3; 使用鏈式呼叫的原因是 每次呼叫後都會返回promise物件;

理解Promise.catch()方法

Promise.catch()方法是promise.then(undefined,onRejected)方法的一個別名,該方法用來註冊當promise物件狀態變為Rejected的回撥函式。

如下程式碼:

var promise = Promise.reject(new Error("message"));
promise.catch(function(error){
    console.log(error);
});

列印如下所示:

理解每次呼叫then都會返回一個新建立的promise物件

不管是then還是catch方法呼叫,都返回一個新的promise物件;

下面我們來看看程式碼如下:

var promise1 = new Promise(function(resolve){
    resolve(1);
});
var thenPromise = promise1.then(function(value){
    console.log(value);
});
var catchPromise = thenPromise.catch(function(error){
    console.log(error);
});
console.log(promise1 !== thenPromise); // true
console.log(thenPromise !== catchPromise); //true

如上程式碼,列印的都是true,這說明不管是then還是catch都返回了和新建立的promise是不同的物件;

如果我們知道了then方法每次都會建立返回一個新的promise物件的話,那麼久不難理解下面的程式碼了;如下:

var promise1 = new Promise(function(resolve){
    resolve(1);
});
promise1.then(function(value){
    return value * 2;
});
promise1.then(function(value){
    return value * 2;
});
promise1.then(function(value){
    console.log("1"+value);
});

如上的程式碼;列印出11;因為他們每次呼叫then方法時,是使用的不同的promise物件;因此最後列印的value還是1;但是如果我們then方法是連續呼叫的話,那情況就不一樣了,比如如下程式碼:

var promise1 = new Promise(function(resolve){
    resolve(2);
});
promise1.then(function(value){
    return value * 2;
}).then(function(value){
    return value * 2;
}).then(function(value){
    console.log("1"+value);
});

列印出18,即 "1" + 2*2*2 = 18;

上面第一種方法沒有使用方法鏈的呼叫,上面第一種那種寫法then 呼叫幾乎是同時開始進行的,且傳給每個then的value都是1;

第二種方式是使用方法鏈的then,使多個then方法連線在一起了,因此函式會嚴格執行 resolve -- then --- then -- then的順序執行,並且傳遞每個then方法的value的值都是前一個promise物件中return的值;因此最後的結果就是18了;

現在我們再回過頭一剛開始我們討論的為什麼要使用promise的原因的問題了,比如2個ajax請求,後一個ajax請求需要獲取到前一個ajax請求的資料,我們之前在使用jquery寫程式碼是如下的:

$.ajax({
   url: '',
   dataType:'json',
   success: function(data) {
    // 獲取data資料 傳給下一個請求
    var id = data.id;
    $.ajax({
        url:'',
        data:{"id":id},
        success:function(){
            // .....
        }
    });
  }
});

現在我們學習了then方法後,我們可以重新編寫上面的程式碼變成如下:(程式碼改成如下這樣的,2018-8-10更新的)

function ajaxPromise(url, data) {
  return new Promise(function(resolve, reject) {
    $.ajax({
      url: url,
      contentType: 'json',
      data: data,
      success: function(resData) {
        resolve(resData);
      }
    })              
  })
}
ajaxPromise('https://cnodejs.org/api/v1/topics', {}).then(function(res) {
  console.log(res);
  var id = res.data[0].id;
  return id;
}).then(function(id) {
  console.log(1111);
  console.log(id);
  ajaxPromise('https://cnodejs.org/api/v1/topics' + id, {});
});

理解Promise.all

Promise.all可以接受一個元素為Promise物件的陣列作為引數,當這個陣列裡面所有的promise物件都變為resolve時,該方法才會返回。

如下程式碼:

var promise1 = new Promise(function(resolve){
    setTimeout(function(){
        resolve(1);
    },3000);
});
var promise2 = new Promise(function(resolve){
    setTimeout(function(){
        resolve(2);
    },1000);
});
Promise.all([promise1,promise2]).then(function(value){
    console.log(value); // 列印[1,2]
});

如上程式碼 列印的是[1,2]; 如上我們看到promise1物件中的setTimeout是3秒的時間,而promise2物件中的setTimeout是1秒的時間,但是在Promise.all方法中會按照陣列的原先順序將結果返回;

在我們平時的需求中,或許有這種情況的需求,比如我們需要發2個ajax請求時,不管他們的先後順序,當這2個ajax請求都同時成功後,我們需要執行某些操作的情況下,這種情況非常適合; 

理解Promise.race

如上可知:Promise.all 在接收到的所有物件promise都變為FulFilled或者 Rejected狀態之後才會繼續後面的處理,但是Promise.race的含義是隻要有一個promise物件進入FulFilled或者Rejected狀態的話,程式就會停止,且會繼續後面的處理邏輯;

如下程式碼:

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

如上程式碼建立了4個promise物件,這些promise物件分別在1ms,32ms,64ms,128ms後變為確定狀態,並且在第一個變為確定狀態後1ms後,then函式就會被呼叫,這時候resolve()方法給傳遞的值為1,因此執行then的回撥函式後,值變為1;

我們再來看看當一個promise物件變為確定狀態(FulFiled)的時候,他們後面的promise物件是否還在執行呢?我們繼續看如下程式碼執行:

var runPromise = new Promise(function(resolve){
    setTimeout(function(){
        console.log(1);
        resolve(2);
    },500);
});
var runPromise2 = new Promise(function(resolve){
    setTimeout(function(){
        console.log(3);
        resolve(4);
    },1000);
});
// 第一個promise變為resolve後程式停止
Promise.race([runPromise,runPromise2]).then(function(value){
    console.log(value);
});

   如上程式碼是使用定時器呼叫的,上面是2個promise物件,我們看到第一個promise物件過500毫秒後加入到執行佇列裡面去,如果執行佇列沒有其他執行緒在執行的時候,就執行該定時器,所以第一次列印1,然後呼叫resolve(2); 接著呼叫promise.race方法,該方法只要有一個變為成功狀態(FulFiled)的時候,程式就會停止,因此列印出2,同時後面的promise物件接著執行,因此列印出3,但是由於promise.race()該方法已經停止呼叫了,所以resolve(4)不會有任何輸出;因此最後輸出的是1,2,3;

由此我們得出結論,當一個promise物件變為(FulFilled)成功狀態的時候,後面的promise物件並沒有停止執行。

Deferred和Promise的關係

Deferred 包含 Promise;

Deferred具備Promise的狀態進行操作的特權方法;

下面我們來看看使用promise來實現deferred;如下程式碼:

function Deferred(){
    this.promise = new Promise(function(resolve,reject){
        this._resolve = resolve;
        this._reject = reject;
    }.bind(this));
}
Deferred.prototype.resolve = function(value) {
    this._resolve.call(this.promise,value);
};
Deferred.prototype.reject = function(reason) {
    this._reject.call(this.promise,reason);
};
function getURL(URL){
    var deferred = new Deferred();
    var req = new XMLHttpRequest();
    req.open('GET',URL,true);
    req.onload = function(){
        if(req.status === 200) {
            deferred.resolve(req.responseText);
        }else {
            deferred.reject(new Error(req.statusText));
        }
    };
    req.onerror = function(){
        deferred.reject(new Error(req.statusText));
    };
    req.send();
    return deferred.promise;
}
var URL = 'http://127.0.0.1/promise/promise.php';
getURL(URL).then(function onFulfilled(value){
    console.log(value);
});

其中promise.php程式碼輸出的是一個json的資料,程式碼如下:

<?php 
    $data = json_decode(file_get_contents("php://input"));
    header("Content-Type: application/json; charset=utf-8");
    echo ('{"id" : ' . $data->id . ', "age" : 24, "sex" : "boy", "name" : "huangxueming"}');
?>

最後執行列印console的出來是:

{"id" : , "age" : 24, "sex" : "boy", "name" : "huangxueming"}

使用promise封裝deferred的方法,無非就是使用promise物件中的resolve和Reject等呼叫方法,下面我們再來看看使用promise物件對ajax請求的封裝如下:

function getURL(URL){
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var URL = 'http://127.0.0.1/promise/promise.php';
getURL(URL).then(function onFulfilled(value){
    console.log(value);
});

上面分別兩種方式使用promise物件實現ajax請求的封裝對比如下:

 Deferred那種方式不需要將promise程式碼括起來。

Promise代表了一個物件,這個物件的狀態現在還不確定,但是未來一個時間點它的狀態要麼變為正常值(FulFilled),要麼變為異常值(Rejected);而Deferred物件表示了一個處理還沒有結束的這種事實,在它的處理結束的時候,可以通過Promise來取得處理結果。

相關文章