Promise一些小總結

sandq發表於2018-01-05

在前端開發中,有個很熟悉的詞叫做“回撥”,在處理一些非同步的函式的時候,回撥被廣泛應用,但是大量用回撥來程式設計,會出現巢狀層級過多,程式碼風格不規範,不清晰的問題。“Promise/A+規範”是一種很方便的非同步程式設計方式。


使用promise進行程式設計有哪些好處?

  • 將複雜的非同步處理輕鬆地進行模式化
  • 程式碼更清晰
  • 異常處理更方便
  • 程式碼鏈式操作,

先來一段簡單的promise的程式碼:

var promise = getAsyncPromise("fileA.txt"); 
promise.then(function(result){
// 獲取檔案內容成功時的處理
}).catch(function(error){
// 獲取檔案內容失敗時的處理
});
複製程式碼

通過這個程式碼,是不是覺得promise非同步程式設計很清晰?

下面我詳細介紹下promise實用的方法。


Promise 簡介

1. 構建promise物件

  • Promise類似於 XMLHttpRequest ,從建構函式 Promise 來建立一個新建新 promise 對 象作為介面。
  • 要想建立一個promise物件、可以使用 new 來呼叫 Promise 的構造器來進行例項化。如下:
var promise = new Promise(function(resolve, reject) { 
// 非同步處理
// 處理結束後、呼叫resolve 或 reject
});
複製程式碼

2. 常用的方法

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

//resolve(成功)時 onFulfilled 會被呼叫
//reject(失敗)時onRejected 會被呼叫
//onFulfilled 、 onRejected 兩個都為可選引數。
複製程式碼
  • promise.then成功和失敗時都可以使用。另外在只想對異常進行處理時可以採用promise.then(undefined, onRejected),這種方式,只指定reject時的回撥函式即可。 不過這種情況下 promise.catch(onRejected) 應該是個更好的選擇。
promise.catch(onRejected)
複製程式碼

3. 靜態方法

  • 包括 Promise.all() 還有 Promise.resolve() 等在內,主要都是一些對Promise進行操作的 輔助方法。

4. 來個栗子

function asyncFunction() {
    return new Promise(function (resolve, reject) { //①
    setTimeout(function () {
        resolve('Async Hello world'); }, 16); //②
    }); 
}
asyncFunction().then(function (value) { console.log(value); // => 'Async Hello world'
}).catch(function (error) { console.log(error);
});
複製程式碼

栗子說明:

  1. new Promise構造器之後,會返回一個promise物件
  2. 為promise物件用設定 .then 呼叫返回值時的回撥函式。
  3. 這個函式會返回promise物件,對於這個promise物件,我們呼叫的 方法來設定resolve後的回撥函式,catch方法來設定發生錯誤時的回撥函式。
  4. 在這種情況下catch的回撥函式並不會被執行(因為promise返回了resolve), 不過如果執行環境沒有提供setTimeout函式的話,那麼上面程式碼在執行中就會產生異常,在catch中設定的回撥函式就會被執行。

上面的程式碼也也可以不用catch方法,用then(resolve, reject)的形式(鏈式操作時最好用catch,可以統一捕捉異常):

asyncFunction().then(function (value) { console.log(value);
}, function (error) { console.log(error);
});
複製程式碼

Promise 狀態詳解

大致的瞭解promise處理流程,單獨介紹一下promise狀態

1. 大致三個狀態

  • resolve(成功)時。此時會呼叫 onFulfilled
  • reject(失敗)時。此時會呼叫 onRejected
  • 既不是resolve也不是reject的狀態。也就是promise物件剛被建立後的初始化狀態等

此處輸入圖片的描述

*注意:從Pending轉換為Fulfilled或Rejected之後, 這個promise物件的狀 態就不會再發生任何變化。

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

編寫Promise程式碼:

1. 建立promise物件

  1. new Promise(fn) 返回一個promise物件
  2. 在 fn 中指定非同步等處理 • 處理結果正常的話,呼叫 resolve(處理結果值) • 處理結果錯誤的話,呼叫 reject(Error物件)

來個XHR的promise物件的栗子

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();  //栗子就不做ie相容了
        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://httpbin.org/get"; 
getURL(URL).then(function onFulfilled(value) {
console.log(value); }).catch(function onRejected(error) {
console.error(error); });

複製程式碼

栗子說明:

  1. getURL 只有在通過XHR取得結果狀態為200時才會呼叫 resolve - 也就是隻有資料取 得成功時,而其他情況(取得失敗)時則會呼叫 reject 方法。
  2. resolve(req.responseText) 在response的內容中加入了引數。 resolve方法的引數並沒有特 別的規則,基本上把要傳給回撥函式引數放進去就可以了。 ( then 方法可以接收到這 個引數值)

6. 編寫promise物件處理方法

讓我們在實際中使用一下剛才建立的返回promise物件的函式

getURL("http://example.com/"); // => 返回promise物件
複製程式碼

為promise物件新增處理方法主要有以下兩種:

  • promise物件被 resolve 時的處理(onFulfilled)
  • promise物件被 reject時的處理(onRejected)
getURL(URL).then(onFulfilled, onRejected); //純then寫法
getURL(URL).then( onFulfilled).catch( onRejected);  // then chatch寫法
複製程式碼

注:一般說來,使用 .catch 來將resolve和reject處理分開來寫是比較推薦的做法, 這兩者的 區別會在then和catch的區別中再做詳細介紹。


詳解promise

1. 使用 Promise.resolve

  • Promise.resolve(value) 可以認為是 new Promise() 方法的快捷方式。
//比如 Promise.resolve(42); 可以認為是以下程式碼的語法糖。
new Promise(function(resolve){ 
    resolve(42);
});
複製程式碼

在這段程式碼中的 resolve(42);會讓這個promise物件立即進入確定(即resolved)狀態,並將 42 傳遞給後面then裡所指定的 onFulfilled 函式。

Promise.resolve(42).then(
    function (value) {
        console.log(value)  //42
    }
);
複製程式碼

:Promise.resolve作為newPromise()的快捷方式,在進行promise物件的初始化或者編寫 測試程式碼的時候都非常方便。


  • Promise.resolve方法另一個作用就是將thenable物件轉換為promise物件。

    **thenable物件**:簡單來說它就是一個非常類似promise的東西(*就像我們有時稱具有.length方法的非陣列物件為Arraylike一樣,thenable指的是一個具有.then 方法的物件*。)   
    複製程式碼
    • 最簡單的例子就是jQuery.ajax(),它的返回值就是thenable的(因為 jQuery.ajax()的返回值是jqXHRObject物件,這個物件具有 .then 方法)

    • 用Promise.resolve來轉換為一個promise物件。變成了promse物件的話,就能直接使用 then 或者 catch等這些在ES6Promises裡定 義的方法了。

var promise = Promise.resolve(
    $.ajax('/json/comment.json')
) // => promise物件 

promise.then(function(value){
    console.log(value); 
});

複製程式碼

總結:簡單總結一下 Promise.resolve 方法的話,可以認為它的作用就是將傳遞給它的引數填 充(Fulfilled)到promise物件後並返回這個promise物件。


2. 使用 Promise.reject

  • Promise.reject(error) 是和 Promise.resolve(value) 類似的靜態方法,是 new Promise() 方 法的快捷方式。
new Promise(
    function (resolve,reject) { 
        reject(new Error("出錯了"));
    }
);
// 是Promise.reject(newError("出錯了"))的語法糖。
//如下:
Promise.reject(new Error("BOOM!")).catch(
    function(error){ 
        console.error(error);
    }
);
複製程式碼

上一篇寫了一部分,再寫一些關於promise的筆記

Promise.then() &&鏈式寫法

  • promise可以寫成方法鏈的形式
aPromise.then(function taskA(value) { 
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){ //最後catch 統一捕捉異常
console.log(error);
});

複製程式碼

來段程式碼,看下鏈式操作的執行流程

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);

複製程式碼

上面這段程式碼的執行流程如下圖:

此處輸入圖片的描述

註釋: .cntch() 可以捕捉 taskA taskB出現的異常,但是 onRejected、finalTask這兩個後面沒有catch捕捉了,所以這兩個函式出現問題不會被捕捉到。

  • 用then、catch 鏈式寫法,要比純then寫法好!!
    • catch 可以捕捉到resolve函式裡面的錯誤,有時候我可以主動在resolve函式裡throw一個錯誤或者返回一個Rejected狀態的promise物件(推薦後者);
    • 鏈式操作程式碼更簡潔

Promise鏈式寫法中引數如何傳遞

前面例子中的Task都是相互獨立的,只是被簡單呼叫而已。這時候如果 Task A 想給 Task B 傳遞一個引數,那就是在 Task A 中 return 的返回值,會在 Task B 執行時傳給它。

function doubleUp(value) { 
    return value * 2;
}
function increment(value) {
    return value + 1; 
}
function output(value) { 
    console.log(value);
}
var promise = Promise.resolve(1); 
promise
.then(increment) 
.then(doubleUp) 
.then(output) 
.catch(function(error){
    // promise chain中出現異常的時候會被呼叫
    console.error(error); 
});

 1. Promise.resolve(1); 傳遞 1 給 increment 函式
 2. 函式 increment 對接收的引數進行 +1 操作並返回(通過 return ) 3. 這時引數變為2,並再次傳給 doubleUp 函式
 3. 最後在函式 output 中列印結果
複製程式碼

**注:**每個方法中return的值不僅只侷限於字串或者數值型別,也可以是物件或者promise 物件等複雜型別。

  1. return的值會由Promise.resolve進行相應的包裝處理,因此不回撥函式中會返回一個什麼樣的值,最終的結果都是返回一個新建立的promise物件。

Promise.catch 的一些小問題

  1. IE8 相容問題catch是ECMAScrip3中保留字是不能作為物件的屬性名使用的(ie8大概是基於es3實現的)可以用Promise['catch']來呼叫(不過很多壓縮、打包工具自帶轉換功能,還不錯)

Promise處理多個非同步任務,promise.all 上場

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

// `delay`毫秒後執行resolve 
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay); }, delay
        );
    }); 
}

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

**注:**從總用時120ms來看,傳遞給Promise.all的promise並不是一個個的順序執行的,而是 同時開始、並行執行的。


Promise處理多個非同步任務,Promise.race 上場

它的使用方法和Promise.all一樣,接收一個promise物件陣列為引數

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

Promise.race([
timerPromisefy(1), 
timerPromisefy(32), 
timerPromisefy(64), 
timerPromisefy(128)
]).then(function (value) { 
    console.log(value); // => 1 只列印出一個最先出結果的
});

複製程式碼

相關文章