47. ES6+ Promise 進階版
1. 前言
前兩節我們學習了 Promise 的用法,并且在上一節我們動手實現了一個符合 Promise A+ 規范的簡版 Promise。真正了解了 Promise 底層是怎么來實現的,更好地幫助我們理解 Promise 并對 Promise 的擴張打下了基礎。對 Promise 的擴展會可以解決一些通用的問題,比如使用?Promise.all()
?去并發請求接口。在 node 中還提供了將?callback
?類型的 api 轉換為 Promise 對象。
本節我們將繼續學習 Promise 對象相關API的使用。這些api在我們的實際應用中會經常使用,并且可以很好的解決常見的問題。
2. Promise.resolve () 和 Promise.reject ()
前面我們已經學習了在?new Promise()
?對象時執行器會提供兩個回調函數,一個是?resolve
?返回一個立即成功的 Promise,一個是?reject
?返回一個立即失敗的 Promise。在執行器中需要根據不同情況調?resolve
?或?reject
?,如果我們只想返回一個成功或失敗的 Promise 怎么做呢?
Promise 對象上的提供了?Promise.resolve(value)
?和?Promise.reject(reason)
?語法糖,用于只返回一個成功或失敗的 Promise。下面我們看下它的對比寫法:
const p1 = new Promise(function(resolve, reject){ reslove(100) }) const p2 = Promise.resolve(100) //和p1的寫法一樣 const p3 = new Promise(function(resolve, reject){ reject('error') }) const p4 = Promise.reject('error') //和p3的寫法一樣
通過上面的對比?Promise.resolve(value)
?創建的實例也具有 then 方法的鏈式調用。這里有個概念就是:如果一個函數或對象,具有 then 方法,那么他就是 thenable 對象。
Promise.resolve(123).then((value) => { console.log(value); }); Promise.reject(new Error('error')).then(() => { // 這里不會走 then 的成功回調 }, (err) => { console.error(err); });
其實,實現?Promise.resolve(value)
?和?Promise.reject(reason)
?的源碼是很簡單的。就是在 Promise 類上創建?resolve
?和?reject
?這個兩個方法,然后去實例化一個 Promise 對象,最后分別在執行器中的?resolve()
?和?reject()
?函數。按照這個思路有如下實現方式:
class Promise { ... resolve(value) { return new Promise((resolve, reject) => { resolve(value) }) } reject(reason) { return new Promise((resolve, reject) => { reject(reason) }) } }
通過上面的實現源碼我們很容易地知道,這兩個方法的用法。需要注意的是?Promise.resolve(value)
?中的 value 是一個 Promise 對象 或者一個 thenable 對象,Promise.reject(reason)
?傳入的是一個異常的原因。
3. catch()
Promise 對象提供了鏈式調用的 catch 方法捕獲上一層錯誤,并返回一個 Promise 對象。catch 其實就是 then 的一個別名,目的是為了更好地捕獲錯誤。它的行為和?Promise.prototype.then(undefined, onRejected)
?只接收?onRejected
?回調是相同的,then 第二個參數是捕獲失敗的回調。所以我們可以實現一個 catch 的源碼,如下:
class Promise { //... catch(errorCallback) { return this.then(null, errorCallback); } }
從上面的實現 catch 的方法我們可以知道,catch 是內部調用了 then 方法并把傳入的回調傳入到 then 的第二個參數中,并返回這個 Promise。這樣我們就更清楚地知道 catch 的內部原理了,以后看到 catch 可以直接把它看成調用了 then 的失敗的回調就行。下面我們看幾個使用 catch 的例子:
let promise = new Promise((resolve, reject) => { resolve('100'); }) promise.then((data) => { console.log('data:', data); // data: 100 throw new Error('error') }, null).catch(reason => { console.log(reason) // Error: error })
catch 后還可以鏈式調用then方法,默認會返回 undefined。也可以返回一個普通的值或者是一個新的 Promise 實例。同樣,在 catch 中如果返回的是一個普通值或者是 resolve,在下一層還是會被 then 的成功回調所捕獲。如果在 catch 中拋出異?;蚴菆绦?reject 則會被下一層 then 的失敗的回調所捕獲。
promise.then((data) => { console.log('data:', data); // data: 100 throw new Error('error') }, null).catch(reason => { console.log(reason) // Error: error return 200 }).then((value) => { console.log(value) // 200 }, null)
4. finally()
finally
是 ES9 的規范,它也是 then 的一個別名,只是這個方法是一定會執行的,不像上面提到的 catch 只有在上一層拋出異?;蚴菆绦?reject 時才會走到 catch 中。
Promise.resolve('123').finally(() => { console.log('100'); // 100 })
知道 finally
是 then 的一個別名,那我們就知道在它后面也是可以鏈式調用的。
Promise.resolve('123').finally(() => { console.log('100'); return 200; }).then((data) => { console.log(data); // 123 })
需要注意的是在 finally
中返回的普通值或是返回一個 Promise 對象,是不會傳到下一個鏈式調用的 then
中的。如果 finally 中返回的是一個異步的 Promise 對象,那么鏈式調用的下一層 then
是要等待 finally
有返回結果后才會執行:
Promise.resolve('123').finally(() => { console.log('100'); return new Promise((resolve, reject) => { setTimeout(() => { resolve(100); }, 3000) }) }).then((data) => { console.log(data); // 123 })
執行上面的代碼,在 then 中打印的結果會在 3 秒后執行。這也說明了 finally 有類似 sleep 函數的意思。
finally
是 ES9 的規范,在不兼容 ES9 的瀏覽器中就不能使用這個 api,所以我們可以在 Promise 對象的原型上增加一個 finally
方法。
Promise.prototype.finally = function(callback) { return this.then((value) => { return Promise.resolve(callback()).then(() => value); }, (err) => { return Promise.reject(callback()).catch(() => {throw err}); }) }
因為 finally 是一定會執行的,所以 then 中的成功和失敗的回調都需要執行 finally 的回調函數。使用?Promise.resolve(value)
?和?Promise.reject(reason)
?去執行 finally 傳入的回調函數,然后使用 then 和 catch 來返回 finally 上一層返回的結果。
3. Promise.all () 和 Promise.race ()
在前端面試中經常會問這兩個 api 并做對比,因為它們的參數都是傳入一個數組,都是做并發請求使用的。
3.1 Promise.all()
Promise.all()
?特點是將多個 Promise 實例包裝成一個新的 Promise 實例,只有同時成功才會返回成功的結果,如果有一個失敗了就會返回失敗,在使用 then 中拿到的也是一個數組,數組的順序和傳入的順序是一致的。
const p1 = Promse.resolve('任務1'); const p2 = Promse.resolve('任務2'); const p3 = Promse.reject('任務失敗'); Promise.all([p1, p2]).then((res) => { console.log(res); // ['任務1', '任務2'] }).catch((error) => { console.log(error) }) Promise.all([p1, p3, p2]).then((result) => { console.log(result) }).catch((error) => { console.log(error) // 任務失敗 })
Promise.all()
?在處理多個任務時是非常有用的,比如?Promise 基礎?一節中使用?Promise.all()
?并發的請求接口的案例,我們希望得到所以接口請求回來的數據之后再去做一些邏輯,這樣我們就不需要維護一個數據來記錄接口請求有沒有完成,而且這樣請求的好處是最大限度地利用瀏覽器的并發請求,節約時間。
3.2 Promise.race()
Promise.race()
?和?Promise.all()
?一樣也是包裝多個 Promise 實例,返回一個新的 Promise 實例,只是返回的結果不同。Promise.all()
?是所有的任務都處理完才會得到結果,而?Promise.race()
?是只要任務成功就返回結果,無論結果是成功還是失敗。
const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('任務1成功...'); }, 1000) }) const p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('任務2成功...'); }, 1500) }) const p3 = new Promise((resolve, reject) => { setTimeout(() => { reject('任務失敗...'); }, 500) }) Promise.race([p1, p2]).then((res) => { console.log(res); // 任務1成功... }).catch((err) => { console.log(err); }) Promise.race([p1, p2, p3]).then((res) => { console.log(res) }).catch((err) => { console.log(err) // 任務失敗... })
上面的實例代碼充分的展示了?Promise.race()
?特性,在實際的開發中很少用到這個 api,這個 api 能做什么用呢?其實這個 api 可以用在一些請求超時時的處理。
當我們瀏覽網頁時,突然網絡斷開或是變得很差的情況下,可以用于提示用戶網絡不佳,這也是一個比較常見的情況。這個時候我們就可以使用?Promise.race()
?來處理:
const request = new Promise((resolve, reject) => { setTimeout(() => { resolve('請求成功...'); }, 3000); }) const timeout = new Promise((resolve, reject) => { setTimeout(() => { reject('請求超時,請檢查網絡...'); }, 2000); }) Promise.race([request, timeout]).then(res => { console.log(res); }, err => { console.log(err); // 請求超時,請檢查網絡... })
上面的代碼中定義了兩個 Promise 實例,一個是請求實例,一個是超時實例。請求實例當 3 秒的時候才會返回,而超時設置了 2 秒,所以會先返回超時的結果,這樣就可以去提醒用戶了。
3.3 實現 Promise.all ()
面試題:實現一個?Promise.all()
?方法。
前面我們說到了 thenable 對象,也就是判斷一個值是不是 Promise 對象,就是判斷它是函數或對象,并具有 then 方法。
const isPromise = (val) => { if (typeof val === "function" || (typeof val == "object" && val !== null)) { if (typeof val.then === "function") { return true; } } return false; };
Promise.all()
?會接收一個數組,數組的每一項都是一個 Promise 實例,并且它的返回結果也是一個 Promise,所以我們需要在內部 new 一個 Promise 對象,并返回。在執行器中我們的目標是:
- 當有實例中有錯誤或拋出異常時,就要執行執行器中的 reject;
- 沒有錯誤時,只有所有的實例都成功時才會執行執行器中的 resolve。
基于這兩點,有如下步驟:
- 內部創建一個計數器,用于記住已經處理的實例,當計數的值和傳入實例的數組長度相等時,執行執行器中的 resolve;
- 創建一個用于存放實例返回結果的數組;
- 處理實例的結果有兩種:一種返回的是普通值、一種返回的是 Promise 對象,然后分別處理;
- 返回普通值結果時直接存放到數組中即可;
- 返回的是一個 Promise 對象時,就需要調用這個實例上的 then 方法得到結果后在存放到結果數組中去。
根據上面的五個步驟基本就可以把?Promise.all()
?實現出來了,具體代碼如下:
Promise.all = function(arr) { return new Promise((resolve, reject) => { let num = 0; // 用于計數 const newArr = []; // 存放最終的結果 function processValue(index, value) { // 處理Promise實例傳入的結果 newArr[index] = value; if (++num == arr.length) { // 當計數器的值和處理的 Promise 實例的長度相當時統一返回保護所以結果的數組 resolve(newArr); } } for (let i = 0; i < arr.length; i++) { const currentValue = arr[i]; // Promise 實例 if (isPromise(currentValue)) { currentValue.then((res) => { processValue(i, res); }, reject) } else { processValue(i, currentValue); } } }); }
上面的代碼已經實現了?Promise.all()
?方法,可以使用下面的例子進行測試。
const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve("任務1成功..."); }, 1000); }); const p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve("任務2成功..."); }, 500); }); Promise.all([p1, p2]).then((res) => { console.log(res) })
4. 小結
本節學習了根據 Promise 衍生出的相關 api 的使用,已經每個 api 基本都給出了實現源碼,理解這些源碼會讓我們更加深刻地理解 Promise,在實際的開發過程中達到游刃有余。到此我們花了三節的時間由淺入深來介紹 Promise,花些時間來徹底弄懂這些知識點,對于我們以后學習其他的異步解決方案有更好的理解。
1. 本站所有文章教程及資源素材均來源于網絡與用戶分享或為本站原創,僅限用于學習和研究。
2. 如果內容損害你的權益請聯系客服QQ:1642748312給予處理。
碼云筆記 » 47. ES6+ Promise 進階版