JS解決回調(diào)地獄為什么需要Promise來優(yōu)化異步編程
為什么需要Promise?
JavaScript在執(zhí)行異步操作時(shí),我們并不知道什么時(shí)候完成,但是我們又需要在這個(gè)異步任務(wù)完成后執(zhí)行一系列動(dòng)作,傳統(tǒng)的做法就是使用回調(diào)函數(shù)來實(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元素下,隨后在頁面上也會(huì)展示出來。
但是如果我們需要在加載完這張圖片后再加載其它的圖片呢,只能在回調(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)換過程是不可逆的,一旦發(fā)生轉(zhuǎn)換,狀態(tài)就不會(huì)再發(fā)生變化了。
實(shí)例方法
promise對(duì)象里面有兩個(gè)函數(shù)用來消費(fèi)執(zhí)行器產(chǎn)生的結(jié)果,分別是then和catch,而finally則用來執(zhí)行清理工作。
then
then這個(gè)函數(shù)接收兩個(gè)函數(shù)作為參數(shù),當(dāng)執(zhí)行器傳遞的結(jié)果狀態(tài)是fulfilled,第一個(gè)函數(shù)參數(shù)會(huì)接收到執(zhí)行器傳遞過來的結(jié)果當(dāng)做參數(shù),并且執(zhí)行;當(dāng)執(zhí)行器傳遞的結(jié)果狀態(tài)為rejected,那么作為第二個(gè)函數(shù)參數(shù)會(huì)收到執(zhí)行器傳遞過來的結(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秒后打印fail
catch
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ù)長得一模一樣,兩種方式有什么差異嗎?
答案是沒有,then(null, errorHandler)
和catch(errorHandler)
這兩種用法都能達(dá)到一樣的效果,都能消費(fèi)執(zhí)行器執(zhí)行失敗時(shí)傳遞的原因。
finally
常規(guī)的try-catch語句有finally語句,在promise中也有finally,它接收一個(gè)函數(shù)作為參數(shù),無論執(zhí)行器得到的結(jié)果狀態(tài)是fulfilled還是rejected,這個(gè)函數(shù)參數(shù)是一定會(huì)被執(zhí)行的。
finally的目的是用來執(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
通過打印結(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ì)上面說到的這兩個(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)用的方式來優(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
用來生成狀態(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
用來生成狀態(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í)間最長,其結(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來實(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)變,無論迭代對(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)) ); };
使用場景
大多數(shù)異步任務(wù)場景都可以使用promise,例如網(wǎng)絡(luò)請(qǐng)求、文件操作、數(shù)據(jù)庫操作等。當(dāng)然不是所有的異步任務(wù)場景都適合使用promise,例如在事件驅(qū)動(dòng)的編程模型中,使用時(shí)間監(jiān)聽器和觸發(fā)器來處理異步操作更加自然和直觀。
在JavaScript中,async和await提供基于promise更高級(jí)的異步編程方式,其使用方式看起來就像同步操作一樣,更加直觀。在使用promise的同時(shí),可以配合async和await體驗(yàn)更好的異步編程。
一個(gè)小問題
我們前面在講catch的時(shí)候說到了,catch(errorHandler)其實(shí)就是then(null, errorHandler)的簡寫,那么下面兩種寫法會(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來優(yōu)化異步編程的詳細(xì)內(nèi)容,更多關(guān)于JS Promise異步編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
layui 實(shí)現(xiàn)table翻頁滾動(dòng)條位置保持不變的例子
今天小編就為大家分享一篇layui 實(shí)現(xiàn)table翻頁滾動(dòng)條位置保持不變的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09js實(shí)現(xiàn)手機(jī)發(fā)送驗(yàn)證碼功能
本文主要介紹了js實(shí)現(xiàn)手機(jī)發(fā)送驗(yàn)證碼功能的示例。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-03-03javascript設(shè)計(jì)模式 – 抽象工廠模式原理與應(yīng)用實(shí)例分析
這篇文章主要介紹了javascript設(shè)計(jì)模式 – 抽象工廠模式,結(jié)合實(shí)例形式分析了javascript抽象工廠模式相關(guān)概念、原理、定義、應(yīng)用場景及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04JavaScript實(shí)現(xiàn)頁面跳轉(zhuǎn)的八種方式
這篇文章介紹了JavaScript實(shí)現(xiàn)頁面跳轉(zhuǎn)的八種方式,文中通過示例代碼介紹的非常詳細(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?的過程,之前在開發(fā)?StarBlog-Admin?的時(shí)候已經(jīng)做了一次封裝,不過那時(shí)是JavaScript跟TypeScript還是有些區(qū)別的,需要的朋友可以參考下2024-01-01JS前端認(rèn)證授權(quán)技巧歸納總結(jié)
這篇文章主要為大家介紹了JS前端認(rèn)證授權(quán)技巧歸納總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03