1. 程式人生 > >JavaScript之Promise物件

JavaScript之Promise物件

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了 Promise 物件。

Promise 物件是一個代理物件(代理一個值),被代理的值在 Promise 物件建立時可能是未知的。它允許你為非同步操作的成功和失敗分別繫結相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的 Promise 物件。 Promise 物件有以下兩個特點:

物件的狀態不受外界影響。Promise 物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功) 和 rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。 Promise 物件的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和 從 pending 變為 rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對 Promise 物件添加回調函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。 基本用法

new Promise( function(resolve, reject) {...} /* executor */  );
  • Promise 物件的初始化接收一個執行函式 executor,executor 是帶有 resolve 和 reject 兩個引數的函式 。

  • Promise 建構函式執行時會立即呼叫 executor 函式, resolve 和 reject 兩個函式作為引數傳遞給 executor(executor 函式在 Promise 建構函式返回新建物件前被呼叫)。

  • resolve 和 reject 函式被呼叫時,分別將 promise 的狀態改為 fulfilled(完成) 或 rejected(失敗)。executor 內部通常會執行一些非同步操作,一旦完成,可以呼叫 resolve 函式來將 promise 狀態改成 fulfilled,或者在發生錯誤時將它的狀態改為 rejected。

如果在 executor 函式中丟擲一個錯誤,那麼該 promise 狀態為 rejected。executor函式的返回值被忽略。

先看個示例:(注:後文的示例均使用 setTimeout 模擬非同步操作)

// 從 pending 變為 fulfilled
var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');
        resolve('promise fulfilled!');
    }, 500);
}).then(function(data) {
    console.log(data);
});
// Hi,
// promise fulfilled!

// 從 pending 變為 rejected
var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');
        reject('promise rejected!');
    }, 500);
}).then(null, function(error) {
    console.log(error); //歡迎加入全棧開發交流圈一起學習交流:864305860 
});                              //面向1-3年前端人員
// Hi,                           //幫助突破技術瓶頸,提升思維能力  
// promise rejected!

從 pending 變為 fulfilled 這段程式碼,當執行 new Promise() 時,傳入的執行函式就立即執行了,此時其內部有一個非同步操作(過 500ms 之後執行),等過了 500ms 之後先執行 console.log(‘Hi,’); 輸出 Hi,,此時 promise 的狀態為 pending(進行中),而執行 resolve(‘Promise!’); 則修改 promise 的狀態為 fulfilled(完成),然後我們呼叫 then() 接收 promise 在 fulfilled 狀態下傳遞的值,此時輸出 ‘Promise!’。

同理,從 pending 變為 rejected 這段程式碼基本差不多,不同的是非同步操作呼叫了 reject 方法,then 方法使用第二個引數接收 rejected 狀態下傳遞的值。

Promise.prototype.then()

then 的作用是為 Promise 例項新增狀態改變時的回撥函式。

then 方法的第一個引數是 resolved 狀態的回撥函式,第二個引數(可選)是 rejected 狀態的回撥函式。

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');

        // 模擬請求,請求狀態為200代表成功,不是200代表失敗
        if (status === 200) {
            resolve('promise fulfilled!');
        } else {
            reject('promise rejected!');
        }
    }, 500);
}).then(function(data) {
    console.log(data);
}, function(error) {                    
    console.log(error);           
});                                      
// 如果呼叫 resolve 方法,輸出如下:
// Hi,
// promise fulfilled!

// 如果呼叫 reject 方法,輸出如下:
// Hi,
// promise rejected!             

then 方法返回的是一個新的 Promise 例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即 then 方法後面再呼叫另一個 then 方法。採用鏈式的 then,可以指定一組按照次序呼叫的回撥函式。這時,前一個回撥函式,有可能返回的還是一個 Promise 物件(即有非同步操作),這時後一個回撥函式,就會等待該 Promise 物件的狀態發生變化,才會被呼叫。

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');
        resolve();
    }, 500);
}).then(function() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            // 模擬請求,請求狀態為200代表成功,不是200代表失敗
            if (status === 200) {
                resolve('promise fulfilled!');
            } else {
                reject('promise rejected!');
            }
        });
    })
}).then(function(data) {
    console.log(data); //歡迎加入全棧開發交流圈一起學習交流:864305860 
}, function(error) {       //面向1-3年前端人員
    console.log(error);  //幫助突破技術瓶頸,提升思維能力  
});
// 如果第一個 then 呼叫 resolve 方法,第二個 then 呼叫第一個回撥函式,最終輸出如下:
// Hi,
// promise fulfilled!

// 如果第一個 then 呼叫 reject 方法,第二個 then 呼叫第一個回撥函式,最終輸出如下:
// Hi,
// promise rejected!
Promise.prototype.catch()

catch 方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。

所以下面程式碼:

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');

        // 模擬請求,請求狀態為200代表成功,不是200代表失敗
        if (status === 200) {
            resolve('promise fulfilled!');
        } else {
            reject('promise rejected!');
        }
    }, 500);
}).then(function(data) {
    console.log(data);
}, function(error) {
    console.log(error);
});

等價於:

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');

        // 模擬請求,請求狀態為200代表成功,不是200代表失敗
        if (status === 200) {
            resolve('promise fulfilled!');
        } else {
            reject('promise rejected!');
        }
    }, 500);
}).then(function(data) {
    console.log(data);
}).catch(function(error) {
    console.log(error);
});

如果沒有使用 catch 方法或者 then 第二個引數指定錯誤處理的回撥函式,Promise 物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應,這跟傳統的 try/catch 程式碼塊是不同。

catch 方法返回的還是一個 Promise 物件,因此後面還可以接著呼叫 then 方法。

catch 方法與 .then(null, rejection) 的不同:

如果非同步操作丟擲錯誤,狀態就會變為 rejected,就會呼叫 catch 方法指定的回撥函式,處理這個錯誤。 then 方法指定的回撥函式,如果執行中丟擲錯誤,也會被 catch 方法捕獲。 catch 方法的寫法更接近同步的寫法(try/catch)。 因此,建議總是使用 catch 方法,而不使用 then 方法的第二個引數。

Promise.prototype.finally()

finally 方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        console.log('Hi,');

        // 模擬請求,請求狀態為200代表成功,不是200代表失敗
        if (status === 200) {
            resolve('promise fulfilled!');
        } else {
            reject('promise rejected!');
        }
    }, 500);
}).then(function(data) {
    console.log(data);
}).catch(function(error) {
    console.log(error);
}).finally(function() {
    console.log('I am finally!');
});

上面程式碼中,不管 promise 最後的狀態,在執行完 then 或 catch 指定的回撥函式以後,都會執行 finally 方法指定的回撥函式。

Promise.all()

Promise.all 方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

var p = Promise.all([p1, p2]);

上面程式碼中,Promise.all 方法接受一個數組作為引數,p1、p2 都是 Promise 例項,如果不是,就會先呼叫下面講到的 Promise.resolve 方法,將引數轉為 Promise 例項,再進一步處理。(Promise.all方法的引數可以不是陣列,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 例項。)

p的狀態由p1、p2決定,分成兩種情況。

  • 只有 p1、p2 的狀態都變成 fulfilled,p 的狀態才會變成 fulfilled,此時 p1、p2 的返回值組成一個數組,傳遞給 p 的回撥函式。

  • 只要 p1、p2 之中有一個被 rejected,p 的狀態就變成 rejected,此時第一個被 reject 的例項的返回值,會傳遞給 p 的回撥函式。

示例:

試想一個頁面聊天系統,我們需要從兩個不同的 URL 分別獲得使用者的個人資訊和好友列表,這兩個任務是可以並行執行的,用Promise.all()實現。

// 並行執行非同步任務
var p1 = new Promise(function (resolve, reject) {
    setTimeout(function() {
        // 模擬請求,請求狀態為200代表成功,不是200代表失敗
        if (status === 200) {
            resolve('P1');
        } else {
            reject('error');
        }
    }, 500);
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
    console.log(results); // 輸出:['P1', 'P2']
}).catch(function(error) {
    console.log(error); // 如果p1執行失敗,則輸出:error
});

注意,如果作為引數的 Promise 例項,自己定義了 catch 方法,那麼它一旦被 rejected,並不會觸發 Promise.all() 的 catch 方法。

Promise.race()

Promise.race 方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

var p = Promise.race([p1, p2]);

上面程式碼中,只要 p1、p2 之中有一個例項率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給 p 的回撥函式。

Promise.race 方法的引數與 Promise.all 方法一樣,如果不是 Promise 例項,就會先呼叫下面講到的 Promise.resolve 方法,將引數轉為 Promise 例項,再進一步處理。

示例:

有些時候,多個非同步任務是為了容錯。比如,同時向兩個 URL 讀取使用者的個人資訊,只需要獲得先返回的結果即可。這種情況下,用Promise.race()實現。

// 多工容錯
var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 400, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
    console.log(result); // 'P2'
});
Promise.resolve()

有時需要將現有物件轉為 Promise 物件,Promise.resolve 方法就起到這個作用。

Promise.resolve方法的引數分成四種情況:

(1)引數是一個 Promise 例項

如果引數是 Promise 例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。

(2)引數是一個 thenable 物件

thenable 物件指的是具有 then 方法的物件,比如下面這個物件。

var thenable = {
    then: function (resolve, reject) {
        resolve(42);
    }
};

Promise.resolve 方法會將這個物件轉為 Promise 物件,然後就立即執行 thenable 物件的 then 方法。

var thenable = {
    then: function (resolve, reject) {
        resolve(42);
    }
};

var p1 = Promise.resolve(thenable);
p1.then(function (value) {
    console.log(value);  // 42
});

上面程式碼中,thenable 物件的 then 方法執行後,物件 p1 的狀態就變為 resolved,從而立即執行最後那個 then 方法指定的回撥函式,輸出 42。

(3)引數不是具有 then 方法的物件,或根本就不是物件

如果引數是一個原始值,或者是一個不具有 then 方法的物件,則 Promise.resolve 方法返回一個新的 Promise 物件,狀態為 resolved。

var p = Promise.resolve('Hello');

p.then(function (s) {
    console.log(s)
});
// 'Hello'

var p1 = Promise.resolve(true);

p1.then(function (b) {
    console.log(b)
});
// true

var p2 = Promise.resolve(1);

p1.then(function (n) {
    console.log(n)
});
// 1

(4)不帶有任何引數

Promise.resolve 方法允許呼叫時不帶引數,直接返回一個 resolved 狀態的 Promise 物件。

所以,如果希望得到一個 Promise 物件,比較方便的方法就是直接呼叫 Promise.resolve 方法。

Promise.reject() Promise.reject 方法也會返回一個新的 Promise 例項,該例項的狀態為 rejected。

注意,Promise.reject 方法的引數,會原封不動地作為 reject 的引數,變成後續方法的引數。這一點與 Promise.resolve 方法不一致。

var thenable = {
    then(resolve, reject) {
        reject('出錯了');
    }
};

Promise.reject(thenable)
    .catch(e = > {
    console.log(e === thenable)
})
// true

上面程式碼中,Promise.reject 方法的引數是一個 thenable 物件,執行以後,後面 catch 方法的引數不是 reject 丟擲的 出錯了 這個字串,而是 thenable 物件。

載入圖片

我們可以將圖片的載入寫成一個 Promise,一旦載入完成,Promise 的狀態就發生變化。

function (path) {
    return new Promise(function (resolve, reject) {
        const image = new Image();
        image.onload = resolve;
        image.onerror = reject;
        image.src = path;
    });
};

封裝ajax

我們可以將 ajax 請求寫成一個 Promise,根據請求的不同狀態改變 Promise 的狀態。

function ajax(method, url, data) {
    var request = new XMLHttpRequest();
    return new Promise(function (resolve, reject) {
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    resolve(request.responseText);
                } else {
                    reject(request.status);
                } //歡迎加入全棧開發交流圈一起學習交流:864305860 
            }    //面向1-3年前端人員
        };      //幫助突破技術瓶頸,提升思維能力  
        request.open(method, url);
        request.send(data);
    });
}

總結 優點:

可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式(回撥地獄)。 在非同步執行的流程中,可以把執行程式碼和處理結果的程式碼清晰地分離開來。

缺點:

無法取消 Promise,一旦新建它就會立即執行,無法中途取消。 如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反應到外部。 當處於 pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

結語

感謝您的觀看,如有不足之處,歡迎批評指正。

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。