JavaScript Promise介紹
前言
眾所周知,在JavaScript的世界中,代碼都是單線程執(zhí)行的。由于這個原因,JavaScript中的耗時操作,如網(wǎng)絡(luò)操作、瀏覽器事件等,都需要異步執(zhí)行。這也導(dǎo)致在JavaScript中異步操作是非常頻繁且常見的。
異步概念
在執(zhí)行某些耗時、不會立即返回結(jié)果的操作時,不會阻塞后面的操作,一旦該耗時操作完成時,立即通知需要調(diào)用其結(jié)果的函數(shù)來做后續(xù)處理。
簡單來理解就是:同步按照寫的代碼順序執(zhí)行,異步不按照代碼順序執(zhí)行
回調(diào)函數(shù)進行異步操作
和同步操作不同,異步操作是不會立即返回結(jié)果的(如發(fā)起網(wǎng)絡(luò)請求,下載文件,操作數(shù)據(jù)庫等)。如果我們后續(xù)的函數(shù)需要之前返回的結(jié)果,又怎樣使之前的異步操作在其完成時通知到后續(xù)函數(shù)來執(zhí)行呢?
通常,我們可以將這個函數(shù)先定義,存儲在內(nèi)存中,將其當(dāng)做參數(shù)傳入之前的異步操作函數(shù)中,等異步操作結(jié)束,就會調(diào)用執(zhí)行這個函數(shù),這個函數(shù)就叫做回調(diào)函數(shù)(callback)。
舉個栗子:
// 下載
function download(callback){
// 模擬異步操作
setTimeout(function(){
// 調(diào)用回調(diào)函數(shù)
callback('下載完成');
}, 1000);
}
function callback(value){
// 下載完成的處理
console.log(value);
}
download(callback);
// 這段代碼將在1秒后在控制臺打印“下載完成”
但假如callback函數(shù)同樣是個異步函數(shù),且callback里又嵌入了callback呢? 例如需求是等待第一個文件下載完成后,再下載第二個文件,等待第二個文件下載完成后,再下載第三個文件...,這樣的話,上面這種方法就不可取了,因為會產(chǎn)生很多的函數(shù)嵌套,嵌套太深容易引發(fā)回調(diào)地獄
Promise進行異步操作
古人云:“君子一諾千金”,這種“承諾將來會執(zhí)行”的對象,在JavaScript中稱為Promise對象。
我們首先看看如何通過Promise改造上面的回調(diào)函數(shù)異步操作
var download = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
resolve('下載完成');
}, 1000);
})
download.then(value => {
console.log(value);
});
// 這段代碼將在1秒后在控制臺打印“下載完成”
可以看到代碼簡潔了一些,當(dāng)然,不只是如此,它還可以連續(xù)調(diào)用,例如我們上面所說的回調(diào)地獄問題,通過Promise可以很簡潔的實現(xiàn)
var download1 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('1');
resolve('文件1下載完成');
}, 1000);
});
var download2 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('2');
resolve('文件2下載完成');
}, 1000);
});
var download3 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('3');
resolve('文件3下載完成');
}, 1000);
});
download1.then(download2).then(download3);
在download1完成之后會調(diào)用download2,download2完成之后會調(diào)用download3,實現(xiàn)任務(wù)串行,為此,對于那些需要連續(xù)執(zhí)行的異步操作,Promise可以是一種很好的解決辦法。
Promise相對于callback,具有更優(yōu)的代碼流,并且具有很好的靈活性。Promise符合自然的事物執(zhí)行順序,即先做異步操作,然后再用.then告知下一步該做什么。而在Callback的用法中,先得知道下一步做什么,然后才能將其作為callback函數(shù)傳入異步操作函數(shù)中。
認(rèn)識Promise
Promise的創(chuàng)建
Promise創(chuàng)建時,會傳給promise一個稱為excutor執(zhí)行器的函數(shù)。這個excutor我們可以理解為生產(chǎn)者的生產(chǎn)過程函數(shù)。這個函數(shù)含有兩個參數(shù)resolve和reject,這倆參數(shù)也同樣是函數(shù),用來傳遞異步操作的結(jié)果
let promise = new Promise(function(resolve, reject) {
// executor
})
有幾點值得說一下:
- 在Promise對象創(chuàng)建時,
excutor會立即執(zhí)行。 - 在
resolve和reject是JS引擎自動創(chuàng)建的函數(shù),我們無需自己創(chuàng)建,只需將其作為參數(shù)傳入就好。
Promise的狀態(tài)與執(zhí)行流程
Promise執(zhí)行流程圖如下:

創(chuàng)建的promise的內(nèi)部狀態(tài)是個對象,初始時為:
{
state, //pending
result, //undefined
}
當(dāng)Promise中的異步任務(wù)執(zhí)行完,要么產(chǎn)生value,要么產(chǎn)生error,此時會立即調(diào)用resolve(當(dāng)產(chǎn)生value時)或者調(diào)用reject(當(dāng)產(chǎn)生error)時,內(nèi)部狀態(tài)也會隨之改變,如下圖所示:

注意,當(dāng)excutor里面即使調(diào)用了多個resolve和reject,其最終還是只執(zhí)行一個,其他的都被忽略掉。
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
多任務(wù)并行
除了串行執(zhí)行若干異步任務(wù)外,Promise還可以并行執(zhí)行異步任務(wù)。
如果一個頁面,需要從多個接口下載文件,但假如這些接口之間沒有依從性,此時我們可以讓多個任務(wù)并行以提升效率。
var download1 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('1');
resolve('文件1下載完成');
}, 500);
});
var download2 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('2');
resolve('文件2下載完成');
}, 800);
});
var download3 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('3');
resolve('文件3下載完成');
}, 1000);
});
Promise.all([download1, download2, download3]).then(function (results) {
console.log(results); // 返回一個數(shù)組,包含三個異步任務(wù)的執(zhí)行結(jié)果
});
多任務(wù)競爭
任務(wù)之間是競爭關(guān)系,使用Promise也可以很簡單的實現(xiàn)。
如果一個頁面,需要從多個接口下載文件,但假如只要其中一個任務(wù)執(zhí)行成功獲取其結(jié)果即可,其它任務(wù)便可丟棄。
var download1 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('1');
resolve('文件1下載完成');
}, 500);
});
var download2 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('2');
resolve('文件2下載完成');
}, 800);
});
var download3 = new Promise(function(resolve, reject) {
// 模擬異步操作
setTimeout(function(){
console.log('3');
resolve('文件3下載完成');
}, 1000);
});
Promise.race([download1, download2, download3]).then(function (results) {
console.log(results); // 返回download1的執(zhí)行結(jié)果
});
由于download1執(zhí)行較快,所以在then中將獲得download1的結(jié)果,但是download2與download3任然會繼續(xù)執(zhí)行,只是執(zhí)行的結(jié)果將會被丟棄。