JS解決回調(diào)地獄為什么需要Promise來(lái)優(yōu)化異步編程

為什么需要Promise?
JavaScript在執(zhí)行異步操作時(shí),我們并不知道什么時(shí)候完成,但是我們又需要在這個(gè)異步任務(wù)完成后執(zhí)行一系列動(dòng)作,傳統(tǒng)的做法就是使用回調(diào)函數(shù)來(lái)實(shí)現(xiàn),下面舉個(gè)常見的例子。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl, callback) {
const img = document.createElement("img");
img.onload = function () {
callback(this);
};
img.src = imgUrl;
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => document.body.appendChild(img)
);
</script>
</html>上面這個(gè)例子會(huì)在圖片加載完成后將圖片放置在body元素下,隨后在頁(yè)面上也會(huì)展示出來(lái)。
但是如果我們需要在加載完這張圖片后再加載其它的圖片呢,只能在回調(diào)函數(shù)里面再次調(diào)用loadImage
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
}
);
}
);繼續(xù)增加一張圖片呢?
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
}
);
}
);
}
);如果按照上述的方式再增加圖片,我們就需要在每層的回調(diào)函數(shù)里面調(diào)用loadImage,就形成了所謂的回調(diào)地獄。

Promise
定義
Promise是一種解決異步編程的方案,它比傳統(tǒng)的異步解決方案更加直觀和靠譜。
狀態(tài)
Promise對(duì)象總共有三種狀態(tài)
- pending:執(zhí)行中,Promise創(chuàng)建后的初始狀態(tài)。
- fulfilled:執(zhí)行成功,異步操作成功取得預(yù)期結(jié)果后的狀態(tài)。
- rejected:執(zhí)行失敗,異步操作失敗未取得預(yù)期結(jié)果后的狀態(tài)。
創(chuàng)建方法
const promise = new Promise((resolve, reject) => {})Promise構(gòu)造函數(shù)接收一個(gè)函數(shù),這個(gè)函數(shù)可以被稱為執(zhí)行器,這個(gè)函數(shù)接收兩個(gè)函數(shù)作為參數(shù),當(dāng)執(zhí)行器有了結(jié)果后,會(huì)調(diào)用兩個(gè)函數(shù)之一。
- resolve:在函數(shù)執(zhí)行成功時(shí)調(diào)用,并且把執(zhí)行器獲取到的結(jié)果當(dāng)成實(shí)參傳遞給它,調(diào)用形式如resolve(獲取到的結(jié)果)
- reject:函數(shù)執(zhí)行失敗時(shí)調(diào)用,并且把具體的失敗原因傳遞給它,調(diào)用形式如reject(失敗原因)
注意:resolve和reject兩個(gè)回調(diào)函數(shù)在Promise類內(nèi)部已經(jīng)定義好函數(shù)體,如果想了解實(shí)現(xiàn)的可以在網(wǎng)上搜索Promise的源碼實(shí)現(xiàn)。
const promise = new Promise((resolve, reject) => {
/* 做一些需要時(shí)間的事,之后調(diào)用可能會(huì)resolve 也可能會(huì)reject */
setTimeout(() => {
const random = Math.random()
console.log(random)
if (random > 0.5) {
resolve('success')
} else {
reject('fail')
}
}, 500)
})
console.log(promise)在瀏覽器控制執(zhí)行上面這段代碼

可以看到剛開始promise的狀態(tài)是pending狀態(tài),500ms后promise的狀態(tài)轉(zhuǎn)變?yōu)閞ejected。
狀態(tài)轉(zhuǎn)換
當(dāng)執(zhí)行器獲取到結(jié)果后,并且調(diào)用resolve或者reject兩個(gè)函數(shù)中的一個(gè),整個(gè)promise對(duì)象的狀態(tài)就會(huì)發(fā)生變化。

這個(gè)狀態(tài)的轉(zhuǎn)換過(guò)程是不可逆的,一旦發(fā)生轉(zhuǎn)換,狀態(tài)就不會(huì)再發(fā)生變化了。
實(shí)例方法
promise對(duì)象里面有兩個(gè)函數(shù)用來(lái)消費(fèi)執(zhí)行器產(chǎn)生的結(jié)果,分別是then和catch,而finally則用來(lái)執(zhí)行清理工作。
then
then這個(gè)函數(shù)接收兩個(gè)函數(shù)作為參數(shù),當(dāng)執(zhí)行器傳遞的結(jié)果狀態(tài)是fulfilled,第一個(gè)函數(shù)參數(shù)會(huì)接收到執(zhí)行器傳遞過(guò)來(lái)的結(jié)果當(dāng)做參數(shù),并且執(zhí)行;當(dāng)執(zhí)行器傳遞的結(jié)果狀態(tài)為rejected,那么作為第二個(gè)函數(shù)參數(shù)會(huì)收到執(zhí)行器傳遞過(guò)來(lái)的結(jié)果當(dāng)做參數(shù),并且執(zhí)行。
const promise = new Promise((resolve, reject) => {
/* 做一些需要時(shí)間的事,之后調(diào)用可能會(huì)resolve 也可能會(huì)reject */
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve("success");
} else {
reject("fail");
}
}, 500);
});
console.log(promise);
promise.then(
(res) => console.log("resolved: ", res), // 生成的隨機(jī)數(shù)大于0.5,則會(huì)執(zhí)行這個(gè)函數(shù)
(err) => console.error("rejected: ", err) // 生成的隨機(jī)數(shù)小于0.5,則會(huì)執(zhí)行這個(gè)函數(shù)
);可以嘗試多次執(zhí)行上面這段代碼,注意控制臺(tái)打印信息,看是否符合上面的結(jié)論。
如果我們只對(duì)成功的情況感興趣,那么我們可以只為then函數(shù)提供一個(gè)函數(shù)參數(shù)。
const promise = new Promise((resolve) => setTimeout(() => resolve("done"), 1000))
promise.then(console.log) // 1秒后打印done如果我們只對(duì)錯(cuò)誤的情況感興趣,那么我們可以為then的第一個(gè)參數(shù)提供null,在第二個(gè)參數(shù)提供具體的函數(shù)
const promise = new Promise((resolve, reject) =>
setTimeout(() => reject("fail"), 1000)
);
promise.then(null, console.log); // 1秒后打印failcatch
catch這個(gè)函數(shù)接收一個(gè)函數(shù)作為參數(shù),當(dāng)執(zhí)行器傳遞的結(jié)果狀態(tài)為rejected,函數(shù)才會(huì)被調(diào)用。
const promise = new Promise((reject) => setTimeout(() => reject("fail"), 500)).catch(
console.log
)
promise.catch(console.log)可能有同學(xué)會(huì)發(fā)現(xiàn),傳遞給catch的參數(shù)好像和傳遞給then的第二個(gè)參數(shù)長(zhǎng)得一模一樣,兩種方式有什么差異嗎?
答案是沒有,then(null, errorHandler)和catch(errorHandler)這兩種用法都能達(dá)到一樣的效果,都能消費(fèi)執(zhí)行器執(zhí)行失敗時(shí)傳遞的原因。
finally
常規(guī)的try-catch語(yǔ)句有finally語(yǔ)句,在promise中也有finally,它接收一個(gè)函數(shù)作為參數(shù),無(wú)論執(zhí)行器得到的結(jié)果狀態(tài)是fulfilled還是rejected,這個(gè)函數(shù)參數(shù)是一定會(huì)被執(zhí)行的。
finally的目的是用來(lái)執(zhí)行清理動(dòng)作的,例如請(qǐng)求已經(jīng)完成,停止顯示loading圖標(biāo)。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve("success");
} else {
reject("fail");
}
}, 500);
});
promise
.finally((res) => {
console.log("======res======", res); // 打印undefined
console.log("task is done, do something");
})
.then(console.log, console.error); // 打印success或者fail通過(guò)打印結(jié)果可以確定兩點(diǎn)
- 傳遞給finally的函數(shù)也不會(huì)接收到執(zhí)行器處理后的結(jié)果。
- finally函數(shù)不參與對(duì)執(zhí)行器產(chǎn)生結(jié)果的消費(fèi),將執(zhí)行器產(chǎn)生的結(jié)果傳遞給后續(xù)的程序去進(jìn)行消費(fèi)。
手動(dòng)實(shí)現(xiàn)下finally函數(shù),對(duì)上面說(shuō)到的這兩個(gè)點(diǎn)就會(huì)非常清晰
Promise.prototype._finally = function (callback) {
return this.then(
(res) => {
callback();
return res;
},
(err) => {
callback();
throw err;
}
);
};鏈?zhǔn)秸{(diào)用
前面介紹的then、catch以及finally函數(shù)在調(diào)用后都會(huì)返回promise對(duì)象,進(jìn)而可以再次調(diào)用then、catch以及finally,這樣就可以進(jìn)行鏈?zhǔn)秸{(diào)用了。
const promise = new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
});
promise
.then((res) => {
console.log(res); // 1
return res * 2;
})
.then((res) => {
console.log(res); // 2
return res * 2;
})
.then((res) => {
console.log(res); // 4
return res * 2;
})
.then((res) => {
console.log(res); // 8
});我們用鏈?zhǔn)秸{(diào)用的方式來(lái)優(yōu)化先前加載圖片的代碼。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
)
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
});
</script>
</html>注意:剛剛接觸promise的同學(xué)不要犯下面這種錯(cuò)誤,下面這種代碼也是回調(diào)地獄的例子。
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
});
});
});靜態(tài)方法
Promise.resolve
用來(lái)生成狀態(tài)為fulfilled的promise對(duì)象,使用方式如下
const promise = Promise.resolve(1) // 生成值為1的promise對(duì)象
代碼實(shí)現(xiàn)如下
Promise.resolve2 = function (value) {
return new Promise((resolve) => {
resolve(value);
});
};Promise.reject
用來(lái)生成狀態(tài)為rejected的promise對(duì)象,使用方式如下
const promise = Promise.reject('fail')) // 錯(cuò)誤原因?yàn)閒ail的promise對(duì)象代碼實(shí)現(xiàn)如下
Promise.reject2 = function(value) {
return new Promise((_, reject) => {
reject(value)
})
}Promise.race
接收一個(gè)可迭代的對(duì)象,并將最先執(zhí)行完成的promise對(duì)象返回。
const promise = Promise.race([ new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]) promise.then(console.log) // 打印2
代碼實(shí)現(xiàn)
Promise.race2 = function (promises) {
return new Promise((resolve, reject) => {
if (!promises[Symbol.iterator]) {
reject(new Error(`${typeof promises} ${promises} is not iterable`));
}
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
};Promise.all
假設(shè)我們希望并行執(zhí)行多個(gè)promise對(duì)象,并等待所有的promise都執(zhí)行成功。
接收一個(gè)可迭代對(duì)象(通常是promise數(shù)組),當(dāng)?shù)鷮?duì)象里面每個(gè)值都被resolve時(shí),會(huì)返回一個(gè)新的promise,并將結(jié)果數(shù)組進(jìn)行返回。當(dāng)?shù)鷮?duì)象里面有任意一個(gè)值被reject時(shí),直接返回新的promise,其狀態(tài)為rejected。
const promise = Promise.all([ new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]) promise.then(console.log, co sole.error) // console.error打印1
這里需要注意一個(gè)點(diǎn),結(jié)果數(shù)組的順序和源promise的順序是一致的,即使前面的promise耗費(fèi)時(shí)間最長(zhǎng),其結(jié)果也會(huì)放置在結(jié)果數(shù)組第一個(gè)。
const promise = Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]) promise.then(console.log) // 打印結(jié)果[1, 2, 3]
我們針對(duì)圖片加載的例子使用Promise.all來(lái)實(shí)現(xiàn)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
const imgUrlList = [
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
];
const promiseList = imgUrlList.map((item) => loadImage(item));
const promise = Promise.all(promiseList).then((imglist) => {
imglist.forEach((item) => document.body.appendChild(item));
});
</script>
</html>代碼實(shí)現(xiàn)
Promise.all2 = function (promises) {
return new Promise((resolve, reject) => {
if (!promises[Symbol.iterator]) {
reject(new Error(`${typeof promises} ${promises} is not iterable`));
}
const len = promises.length;
const result = new Array(len);
let count = 0;
if (!len) {
resolve(result);
return;
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(res) => {
count++;
result[i] = res; // 保證結(jié)果數(shù)組的放置順序
if (count === len) {
resolve(result);
}
},
(err) => {
reject(err);
}
);
}
});
};Promise.allSettled
前面提到的Promise.all遇到任意一個(gè)promise reject,那么Promise.all會(huì)直接返回一個(gè)rejected的promise對(duì)象。而Promise.allSetled只需要等待迭代對(duì)象內(nèi)所有的值都完成了狀態(tài)的轉(zhuǎn)變,無(wú)論迭代對(duì)象里面的值是被resolve還是reject,那么就會(huì)返回一個(gè)狀態(tài)為fulfilled的promise對(duì)象,并以包含對(duì)象數(shù)組的形式返回結(jié)果。
const promise = Promise.allSettled([
new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]);
promise.then(console.log, console.error);
// 打印結(jié)果
// [
// { status: 'rejected', reason: 1 },
// { status: 'fulfilled', value: 2 },
// { status: 'fulfilled', value: 3 }
// ]代碼實(shí)現(xiàn)
Promise.allSettled2 = function (promises) {
const resolveHandler = (res) => ({ status: "fulfilled", value: res });
const rejectHandler = (err) => ({ status: "rejected", reason: err });
return Promise.all(
promises.map((item) => item.then(resolveHandler, rejectHandler))
);
};使用場(chǎng)景
大多數(shù)異步任務(wù)場(chǎng)景都可以使用promise,例如網(wǎng)絡(luò)請(qǐng)求、文件操作、數(shù)據(jù)庫(kù)操作等。當(dāng)然不是所有的異步任務(wù)場(chǎng)景都適合使用promise,例如在事件驅(qū)動(dòng)的編程模型中,使用時(shí)間監(jiān)聽器和觸發(fā)器來(lái)處理異步操作更加自然和直觀。
在JavaScript中,async和await提供基于promise更高級(jí)的異步編程方式,其使用方式看起來(lái)就像同步操作一樣,更加直觀。在使用promise的同時(shí),可以配合async和await體驗(yàn)更好的異步編程。
一個(gè)小問(wèn)題
我們前面在講catch的時(shí)候說(shuō)到了,catch(errorHandler)其實(shí)就是then(null, errorHandler)的簡(jiǎn)寫,那么下面兩種寫法會(huì)有區(qū)別嗎?
// 寫法1 promise.then(resolveHandler, rejectHandler) // 寫法2 promise.then(resolveHandler).catch(rejectHandler)
答案是不一樣,假如在resolveHandler里面拋出錯(cuò)誤,寫法1最終會(huì)獲得一個(gè)rejected的promise,而寫法二由于后續(xù)有catch方法,所以即使f1里面有拋出異常,也能得到處理。
以上就是JS解決回調(diào)地獄為什么需要Promise來(lái)優(yōu)化異步編程的詳細(xì)內(nèi)容,更多關(guān)于JS Promise異步編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
layui 實(shí)現(xiàn)table翻頁(yè)滾動(dòng)條位置保持不變的例子
今天小編就為大家分享一篇layui 實(shí)現(xiàn)table翻頁(yè)滾動(dòng)條位置保持不變的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09
js實(shí)現(xiàn)手機(jī)發(fā)送驗(yàn)證碼功能
本文主要介紹了js實(shí)現(xiàn)手機(jī)發(fā)送驗(yàn)證碼功能的示例。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-03-03
javascript設(shè)計(jì)模式 – 抽象工廠模式原理與應(yīng)用實(shí)例分析
這篇文章主要介紹了javascript設(shè)計(jì)模式 – 抽象工廠模式,結(jié)合實(shí)例形式分析了javascript抽象工廠模式相關(guān)概念、原理、定義、應(yīng)用場(chǎng)景及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04
JavaScript實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)的八種方式
這篇文章介紹了JavaScript實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)的八種方式,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
關(guān)于JS控制代碼暫停的實(shí)現(xiàn)方法分享
關(guān)于JS控制代碼暫停的工作總結(jié),需要的朋友可以參考下2012-10-10
在TypeScript項(xiàng)目中搭配Axios封裝后端接口調(diào)用
這篇文章主要介紹了在TypeScript項(xiàng)目中搭配Axios封裝后端接口調(diào)用,本文記錄一下在?TypeScript?項(xiàng)目里封裝?axios?的過(guò)程,之前在開發(fā)?StarBlog-Admin?的時(shí)候已經(jīng)做了一次封裝,不過(guò)那時(shí)是JavaScript跟TypeScript還是有些區(qū)別的,需要的朋友可以參考下2024-01-01
JS前端認(rèn)證授權(quán)技巧歸納總結(jié)
這篇文章主要為大家介紹了JS前端認(rèn)證授權(quán)技巧歸納總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

