js–promise、async 和 await 相關知識總結
- 2022 年 5 月 15 日
- 筆記
- javascript
前言
promise 是前端開發人員必須掌握的知識點,本文來總結一下相關學習筆記。
正文
1、什麼是prommise,promise 解決了什麼問題
a、promise 是什麼
Promise 是承諾的意思,承諾它過一段時間會給你一個結果。Promise 是一種解決異步編程的方案,相比回調函數和事件更合理和更強大。從語法上講,promise 是一個對象,從它可以獲取異步操作的消息;
promise 有三種狀態:pending 初始狀態也叫等待狀態,fulfiled成功狀態,rejected 失敗狀態;狀態一旦改變,就不會再變。創造 promise實例後,它會立即執行。需要注意,promise 的狀態是不可逆的,一旦狀態由 pending 變為 fulfiled 或者reject 狀態,意味着已經產生了結果,同樣,轉為成功狀態會有成功的結果,轉為失敗狀態會返回失敗的原因。
promise 作為構造函數,接收兩個參數,分別是成功和失敗的回調函數。
b、promise 解決了什麼問題
我們先來看如下代碼,並不陌生
setTimeout(function () { console.log("開始執行"); }, 3000);
上面的代碼,粗略的可以認為在 3 秒後,程序輸出開始執行,但是如果業務比較複雜,我們想在3秒後輸出開始執行,再隔 3 秒打印一次第二次執行呢?接着在隔 3 秒打印第三次執行,代碼會這樣寫:
setTimeout(function () { console.log("開始執行"); setTimeout(function () { console.log("第二次執行"); setTimeout(function () { console.log("第三次執行"); }, 3000); }, 3000); }, 3000);
再看上面的代碼,如果後面的需求再次優化,需要類似的打印第 4,5,6 次呢?我們的代碼還是這樣一層層嵌套起來嗎? 這樣多層函數之間互相嵌套,就產生了回調地獄的問題,這樣寫代碼有個很大的缺點:(1)代碼耦合行太強,牽一髮而動全身,可維護性很差,同樣,大量冗餘的代碼互相嵌套,可讀性很差。因此,為了解決回調地獄的問題,ES6 提出了 Promise。通過 promise 將上面的代碼改裝一下將顯的代碼優雅很多:
function sleep(second) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), second * 1000); }); } sleep(3) .then(() => { console.log("開始執行"); return sleep(3); }) .then(() => { console.log("第二次執行"); return sleep(3); }) .then(() => { console.log("第三次執行"); });
2、ES6 中 promise 的使用
1)then 鏈式調用
從表面上看,Promise只是能夠簡化層層回調的寫法,而實質上,Promise 的精髓是「狀態」,用維護狀態、傳遞狀態的方式來使得回調函數能夠及時調用,它比傳遞 callback 函數要簡單、靈活的多。所以使用 Promise 的正確場景是這樣的:
p.then((data) => { console.log(data); }) .then((data) => { console.log(data); }) .then((data) => { console.log(data); });
then 是實例狀態發生改變時的回調函數,第一個參數是 resolved 狀態的回調函數,第二個參數是 rejected 狀態的回調函數,then方法返回的是一個新的Promise實例,也就是promise能鏈式書寫的原因。默認常寫第一個參數即可,reject 狀態的回調可以通過 catch 來捕獲異常。
2)catch 方法用來指定 promise 實例狀態變為 rejected 的捕獲
catch 捕獲reject狀態的回調,相當於 then中的第二個參數,一般寫法如下:
p.then((data) => { console.log("resolved", data); }).catch((err) => { console.log("rejected", err); });
也就是說進到catch方法裏面去了,而且把錯誤原因傳到了reason參數中。即便是有錯誤的代碼也不會報錯了,這與我們的try/catch語句有相同的功能。
3)all 方法將多個 promise 實例包裝成一個新的 promise 實例(誰跑的慢,以誰為準執行回調)
Promise.all 方法接收一個數組(可迭代對象)作為參數,並且數組中的每個元素都是 Promise 實例,最終返回結果也為一個 Promise 對象,例如:
const p = Promise.all([p1, p2, p3]),實例p的狀態由p1、p2、p3決定,分為兩種:
只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數;
只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數;
常見的寫法如下:
let Promise1 = new Promise(function (resolve, reject) {}); let Promise2 = new Promise(function (resolve, reject) {}); let Promise3 = new Promise(function (resolve, reject) {}); let p = Promise.all([Promise1, Promise2, Promise3]); p.then( (res) => { // 三者都成功則成功,成功後處理 }, (err) => { // 三者只要有失敗的就返回失敗,失敗後處理 } );
有了all 方法,我們就可以並行執行多個異步操作,並且在一個回調中處理所有的返回數據,比如開發中打開網頁時,預先加載需要用到的各種資源如圖片、flash 以及各種靜態文件,所有的都加載完後,我們再進行頁面的初始化。
(4)race 方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例(誰跑的快,以誰為準執行回調)
const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟着改變,率先改變的 Promise 實例的返回值則傳遞給p的回調函數。
使用場景:比如,我們在頁面加載的時候,需要請求後端獲取某個圖片的URL,這裡我們可以設置請求的超時時間,當在設定的時間內後端接口沒有返回時,頁面給出請求超時提示。代碼如下:
//請求某個圖片資源 function requestImg() { var p = new Promise((resolve, reject) => { var img = new Image(); img.onload = function () { resolve(img); } img.src = '圖片的路徑'; }); return p; } //延時函數,用於給請求計時 function timeout() { var p = new Promise((resolve, reject) => { setTimeout(() => { reject('圖片請求超時'); }, 5000); }); return p; } Promise.race([requestImg(), timeout()]).then((data) => { console.log(data); }).catch((err) => { console.log(err); });
3、promise 的缺點
1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消
2)如果不設置回調函數,Promise 內部拋出的錯誤,不會反映到外部
3)當處於 pending(等待)狀態時,無法得知目前進展到哪一個階段,是剛剛開始還是即將完成
4、手動實現 Promise
function resolvePromise(promise2, x, resolve, reject) { //判斷x是不是promise //規範中規定:我們允許別人亂寫,這個代碼可以實現我們的promise和別人的promise 進行交互 if (promise2 === x) {//不能自己等待自己完成 return reject(new TypeError('循環引用')); }; // x是除了null以外的對象或者函數 if (x != null && (typeof x === 'object' || typeof x === 'function')) { let called;//防止成功後調用失敗 try {//防止取then是出現異常 object.defineProperty let then = x.then;//取x的then方法 {then:{}} if (typeof then === 'function') {//如果then是函數就認為他是promise //call第一個參數是this,後面的是成功的回調和失敗的回調 then.call(x, y => {//如果Y是promise就繼續遞歸promise if (called) return; called = true; resolvePromise(promise2, y, resolve, reject) }, r => { //只要失敗了就失敗了 if (called) return; called = true; reject(r); }); } else {//then是一個普通對象,就直接成功即可 resolve(x); } } catch (e) { if (called) return; called = true; reject(e) } } else {//x = 123 x就是一個普通值 作為下個then成功的參數 resolve(x) } } class Promise { constructor(executor) { //默認狀態是等待狀態 this.status = 'panding'; this.value = undefined; this.reason = undefined; //存放成功的回調 this.onResolvedCallbacks = []; //存放失敗的回調 this.onRejectedCallbacks = []; let resolve = (data) => {//this指的是實例 if (this.status === 'pending') { this.value = data; this.status = "resolved"; this.onResolvedCallbacks.forEach(fn => fn()); } } let reject = (reason) => { if (this.status === 'pending') { this.reason = reason; this.status = 'rejected'; this.onRejectedCallbacks.forEach(fn => fn()); } } try {//執行時可能會發生異常 executor(resolve, reject); } catch (e) { reject(e);//promise失敗了 } } then(onFuiFilled, onRejected) { //防止值得穿透 onFuiFilled = typeof onFuiFilled === 'function' ? onFuiFilled : y => y; onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; } let promise2;//作為下一次then方法的promise if (this.status === 'resolved') { promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { //成功的邏輯 失敗的邏輯 let x = onFuiFilled(this.value); //看x是不是promise 如果是promise取他的結果 作為promise2成功的的結果 //如果返回一個普通值,作為promise2成功的結果 //resolvePromise可以解析x和promise2之間的關係 //在resolvePromise中傳入四個參數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。 resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e); } }, 0) }); } if (this.status === 'rejected') { promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(this.reason); //在resolvePromise中傳入四個參數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。 resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e); } }, 0) }); } //當前既沒有完成也沒有失敗 if (this.status === 'pending') { promise2 = new Promise((resolve, reject) => { //把成功的函數一個個存放到成功回調函數數組中 this.onResolvedCallbacks.push(() => { setTimeout(() => { try { let x = onFuiFilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0) }); //把失敗的函數一個個存放到失敗回調函數數組中 this.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) }) }) } return promise2;//調用then後返回一個新的promise } catch(onRejected) { // catch 方法就是then方法沒有成功的簡寫 return this.then(null, onRejected); } } Promise.all = function (promises) { //promises是一個promise的數組 return new Promise(function (resolve, reject) { let arr = []; //arr是最終返回值的結果 let i = 0; // 表示成功了多少次 function processData(index, data) { arr[index] = data; if (++i === promises.length) { resolve(arr); } } for (let i = 0; i < promises.length; i++) { promises[i].then(function (data) { processData(i, data) }, reject) } }) } // 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了 Promise.race = function (promises) { return new Promise((resolve, reject) => { for (var i = 0; i < promises.length; i++) { promises[i].then(resolve, reject) } }) } // 生成一個成功的promise Promise.resolve = function (value) { return new Promise((resolve, reject) => resolve(value); } // 生成一個失敗的promise Promise.reject = function (reason) { return new Promise((resolve, reject) => reject(reason)); } module.exports = Promise;
5、async 和 await相關
async/await 是 ES7 提出的基於 Promise 的解決異步的最終方案。
async 就是 generation 和 promise 的語法糖,async 就是將 generator的*換成 async,將 yiled 換成 await函數前必須加一個 async,異步操作方法前加一個 await 關鍵字,意思就是等一下,執行完了再繼續走,注意:await 只能在 async 函數中運行,否則會報錯,Promise 如果返回的是一個錯誤的結果,如果沒有做異常處理,就會報錯,所以用 try..catch 捕獲一下異常就可以了。
async
async是一個加在函數前的修飾符,被async定義的函數會默認返回一個Promise對象resolve的值。因此對async函數可以直接then,返回值就是then方法傳入的函數。使用如下:
async function fun() { console.log(1); return 1; } fun().then(val => { console.log(val) // 1,1 })
await
await 也是一個修飾符,只能放在async定義的函數內。可以理解為等待。await 修飾的如果是Promise對象:可以獲取Promise中返回的內容(resolve或reject的參數),且取到值後語句才會往下執行;如果不是Promise對象:把這個非promise的東西當做await表達式的結果。使用如下:
async function fun() { let a = await new Promise((resolve, reject) => { setTimeout(function () { resolve('setTimeout promise') }, 3000) }) let b = await "表達式"; let c = await function () { return '函數表達式' }() console.log(a, b, c) } fun(); // 3秒後輸出:"setTimeout promise" "表達式" "函數表達式"
寫在最後
以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。