深入理解Promise從這裡開始

安全劍客發表於2020-12-09
我們都知道 JavaScript 的程式碼執行的時候是跑在單執行緒上的,可以理解為只能按照程式碼的出現順序,從上到下一行一行的執行,但是遇到了非同步的行為,比如定時器(一定時間之後才去執行),那就需要等同步程式碼執行完成後的一段時間裡再去執行非同步程式碼。
從非同步程式設計說起

我們都知道 JavaScript 的程式碼執行的時候是跑在單執行緒上的,可以理解為只能按照程式碼的出現順序,從上到下一行一行的執行,但是遇到了非同步的行為,比如定時器(一定時間之後才去執行),那就需要等同步程式碼執行完成後的一段時間裡再去執行非同步程式碼。

對於同步行為,如下面的程式碼,我們能夠很清楚的知道每一行會發生什麼,這是因為後面的指令總是等到前面的指令執行完成後才去執行,所以這裡的第二行裡的變數 x 在記憶體裡已經是定義過的。

let x = 10;let y = x + 5;

但是對於非同步程式碼,我們就不好推斷到底什麼時候會執行完成了。比如舉一個實際的例子,我們去動態載入某個 ,會這樣做:

function loadScript(src) { 
    let script = document.createElement('script') 
    script.src = src 
    document.head.append(script) 
}

這個 載入完成的時候會去執行定義在指令碼里的一些函式,比如初始化函式 init,那麼我們可以會這樣寫:

function loadScript(src) { 
    let script = document.createElement('script') 
    script.src = src 
    document.head.append(script) 
} 
loadScript('./js/script.js')

init() // 定義在 ./js/script.js 裡的函式
但是實際執行後卻發現,這樣根本不行,因為載入指令碼是需要花時間的,是一個非同步的行為,瀏覽器執行 JavaScript 的時候並不會等到指令碼載入完成的時候再去呼叫 init 函式。

以往,對於這種非同步程式設計的做法通常就是透過給函式傳遞一個回撥函式來處理,上面那個例子可以這樣做:

function loadScript(src, success, fail) { 
    let script = document.createElement('script') 
    script.src = src 
    script.onload = success 
    script.onerror = fail 
    document.head.append(script) 
} 
loadScript('./js/script.js', success, fail) 
function success() { 
    console.log('success') 
    init()  // 定義在 ./js/script.js 中的函式 
} 
function fail() { 
    console.log('fail') 
}

上面這樣做能夠保證在指令碼載入完成的時候,再去執行指令碼里的函式。但是多考慮一個問題,如果 success 裡又需要載入別的 js 檔案呢,那豈不是需要多層巢狀了。是的,這樣的多層巢狀會使得程式碼層次變得更加深入,難以閱讀以及後期維護成本非常高,尤其是當裡面加上了很多的判斷邏輯的時候情況會更加糟糕,這就是所謂的 “回撥地獄”,且又因為它的程式碼形狀很像躺著的金字塔,所以有的人也喜歡叫它 “噩運金字塔”。

而為了避免這類 “回撥地獄” 問題,目前最好的做法之一就是使用 Promise。

Promise正篇

使用 Promise 可以很好的解決上面提到的 “回撥地獄” 問題,直接來看結果:

function loadScript(src) { 
    return new Promise(function(resolve, reject) { 
        let script = document.createElement('script'); 
        script.src = src; 
        script.onload = () => resolve(script); 
        script.onerror = () => reject(new Error(`Script load error for ${src}`)); 
        document.head.append(script); 
    }); 
} 
loadScript('./scripts.js').then(res => { 
    console.log('success', res); 
    init() 
}).catch(err => { 
    console.log(err); 
})

這裡透過使用 Promise 例項的 then 和 catch 函式將多層巢狀的程式碼改成了同步處理流程,看起來效果還是不錯的,那什麼是 Promise 呢?

Promise 首先是一個物件,它通常用於描述現在開始執行,一段時間後才能獲得結果的行為(非同步行為),內部儲存了該非同步行為的結果。然後,它還是一個有狀態的物件:
pending:待定
fulfilled:兌現,有時候也叫解決(resolved)
rejected:拒絕
一個 Promise 只有這 3 種狀態,且狀態的轉換過程有且僅有 2 種:
pending 到 fulfilled
pending 到 rejected
可以透過如下的 Promise 物件構造器來建立一個 Promise:

let promise = new Promise((resolve, reject) => {})

傳遞給 new Promise 的是 executor 執行器。當 Promise 被建立的時候,executor 會立即同步執行。executor 函式里通常做了 2 件事情:初始化一個非同步行為和控制狀態的最終轉換。

new Promise((resolve, reject) => { 
    setTimeout(() => { 
        resolve() 
    }, 1000) 
})

如上程式碼所示,setTimeout 函式用來描述一個非同步行為,而 resolve 用來改變狀態。executor 函式包含 2 個引數,他們都是回撥函式,用於控制 Promise 的狀態轉換:

resolve:用來將狀態 pending 轉換成 fulfilled
reject:用來將狀態 pending 轉換成 rejected
一個 Promise 的狀態一旦被轉換過,則無法再變更:

let p = new Promise((resolve, reject) => { 
    setTimeout(() => { 
        resolve('第一次 resolve') 
        resolve('第二次 resolve')  // 將被忽略 
        reject('第一次 reject')  // 將被忽略 
    }, 0) 
}) 
setTimeout(console.log, 1000, p)  // Promise {: "第一次 resolve"}

可以看到執行了 2 次 resolve 函式和 1 次 reject 函式,但是 promise 的最終結果是取的第一次 resolve 的結果,印證了上面的結論。

由 new Promise 構造器返回的 Promise 物件具有如下內部屬性:

PromiseState:最初是 pending,resolve 被呼叫的時候變為 fulfilled,或者 reject 被呼叫時會變為 rejected;
PromiseResult:最初是 undefined,resolve(value) 被呼叫時變為 value,或者在 reject(error) 被呼叫時變為 error。
比如上面例子中列印出來的 Promise 物件結果中,fulfilled 是其內部的 PromiseState,而 “第一次 resolve” 是其 PromiseResult。

// Promise {: "第一次 resolve"}
Promise例項方法
Promise.prototype.then()

Promise.prototype.then() 將用於為 Promise 例項新增處理程式的函式。它接受 2 個可選的引數:

onResolved:狀態由 pending 轉換成 fulfilled 時執行;
onRejected:狀態由 pending 轉換成 rejected 時執行。

它可以寫成這樣:

function onResolved(res) { 
    console.log('resolved' + res)  // resolved3 
} 
function onRejected(err) { 
    console.log('rejected' + err) 
} 
new Promise((resolve, reject) => { 
    resolve(3) 
}).then(onResolved, onRejected)

或者寫成更簡單的方式:

new Promise((resolve, reject) => { 
    resolve(3) 
}).then(res => { 
    console.log('resolved' + res)  // resolved3 
}, err => { 
    console.log('rejected' + err) 
})

因為狀態的變化只有 2 種,所以 onResolved 和 onRejected 在執行的時候必定是互斥。

上面介紹到了 then() 的引數是可選的,當只有 onResolved 的時候可以這樣寫:

new Promise((resolve, reject) => { 
    resolve() 
}).then(res => {})

當引數只有 onRejected 的時候,需要把第一個引數設定為 null:

new Promise((resolve, reject) => { 
    reject() 
}).then(null, err => {})

如果給 then() 函式傳遞來了非函式引數,則會預設忽略。

Promise.prototype.catch()

Promise.prototype.catch() 用於給 Promise 物件新增拒絕處理程式。只接受一個引數:onRejected 函式。實際上,下面這兩種寫法是等效的:

function onRejected(err){} 
new Promise((resolve, reject) => { 
    reject() 
}).catch(onRejected) 
new Promise((resolve, reject) => { 
    reject() 
}).then(null, onRejected) 
Promise.prototype.finally()
Promise.prototype.finally() 用於給 Promise 物件新增 onFinally 函式,這個函式主要是做一些清理的工作,只有狀態變化的時候才會執行該 onFinally 函式。
function onFinally() { 
    console.log(888)  // 並不會執行   
} 
new Promise((resolve, reject) => { 
     
}).finally(onFinally)

因為 onFinally 函式是沒有任何引數的,所以在其內部其實並不知道該 Promise 的狀態是怎麼樣的。

鏈式呼叫

鏈式呼叫裡涉及到的知識點很多,我們不妨先看看下面這道題,你能正確輸出其列印順序嘛?

new Promise((resolve, reject) => { 
    resolve() 
}).then(() => { 
    console.log('A') 
    new Promise((resolve, reject) => { 
        resolve() 
    }).then(() => { 
        console.log('B') 
    }).then(() => { 
        console.log('C') 
    }) 
}).then(() => { 
    console.log('D') 
})

這裡我不給出答案,希望你能動手敲一敲程式碼,然後思考下為什麼?容我講完這部分知識,相信你能自己理解其中緣由。

從上面這串程式碼裡,我們看到 new Promise 後面接了很多的 .then() 處理程式,這個其實就是 Promise 的鏈式呼叫,那它為什麼能鏈式呼叫呢?

基於onResolved生成一個新的Promise

因為 Promise.prototype.then() 會返回一個新的 Promise,來看下:

let p1 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p2 = p1.then(() => 6) 
setTimeout(console.log, 0, p1)  // Promise {: 3} 
setTimeout(console.log, 0, p2)  // Promise {: 6}

可以看到 p1 和 p2 的內部 PromiseResult 是不一樣的,說明 p2 是一個新的 Promise 例項。

新產生的 Promise 會基於 onResolved 的返回值進行構建,構建的時候其實是把返回值傳遞給 Promise.resolve() 生成的新例項,比如上面那串程式碼裡 p1.then(() => 6) 這裡的 onResolved 函式返回了一個 6 ,所以新的 Promise 的內部值會是 6。

如果 .then() 沒有提供 onResolved 這個處理程式,則 Promise.resolve() 會基於上一個例項 resolve 後的值來初始化一個新的例項:

let p1 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p2 = p1.then() 
setTimeout(console.log, 0, p2)  // Promise {: 3}

如果 onResolved 處理程式沒有返回值,那麼返回的新例項的內部值會是 undefined:

let p1 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p2 = p1.then(() => {}) 
setTimeout(console.log, 0, p2)  // Promise {: undefined}

如果在 onResolved 處理程式裡丟擲異常,則會返回一個新的 rejected 狀態的 Promise:

let p1 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p2 = p1.then(() => { 
    throw new Error('這是一個錯誤')} 
) 
setTimeout(console.log, 0, p2)  // Promise {: 這是一個錯誤}
基於onRejected生成一個新的Promise

基於 onRejected 的返回值也會返回一個新的 Promise,而且處理邏輯也是一樣的,也是透過把返回值傳遞給 Promise.resolve() 產生一個新的例項:

let p1 = new Promise((resolve, reject) => { 
    reject(3) 
}) 
 
// 沒有 `onRejected` 處理程式時,會原樣向後傳,不過是新例項 
let p2 = p1.then(() => {})  s 
setTimeout(console.log, 0, p2)  // Promise {: 3} 
 
// 返回值為undefined時 
let p3 = p1.then(null, () => {})  
setTimeout(console.log, 0, p3)  // Promise {: undefined}  
 
// 返回值有實際值的時候 
let p4 = p1.then(null, () => 6)  
setTimeout(console.log, 0, p4)  // Promise {: 6} 
 
// 當返回值是Promise時,會保留當前Promise 
let p5 = p1.then(null, () => Promise.reject())  
setTimeout(console.log, 0, p5)  // Promise {: undefined}  
 
// 當遇到一個錯誤的時候 
let p6 = p1.then(null, () => { 
    throw new Error('error') 
})  
setTimeout(console.log, 0, p6)  // Promise {: error}  
 
// 當返回值是一個錯誤時 
let p7 = p1.then(null, () => new Error('error'))  
setTimeout(console.log, 0, p7)  // Promise {: Error: error}

這裡你會不會有個疑惑?例項 resolve() 的時候,狀態由 pending 變成 rejected,從而呼叫 onRejected 進行處理,但是為什麼有時候會返回一個 fulfilled 的新例項呢?試著想一下,如果 onRejected 返回了一個 pending 的或者 rejected 狀態的新例項,那後續的鏈式呼叫就進行不下去了,看下面例子:

new Promise((resolve, reject) => { 
    reject() 
}).then(null, () => { 
    console.log('A') 
}).then(() => { 
    console.log('B') 
}).then(() => { 
    console.log('C') 
}).catch(() => { 
    console.log('D') 
})

如果 A 處理函式這裡返回了一個 pending 狀態的新例項,那麼後續所有的鏈式操作都無法執行;或者返回的是一個 rejected 狀態的新例項,那麼後續的 B 和 C 也就無法執行了,那居然都不能執行 B 和 C 所在處理程式,那定義來幹嘛呢?鏈式操作就毫無鏈式可言。又,onRejected 的存在的根本意義無非就是用於捕獲 Promise 產生的錯誤,從而不影響程式的正常執行,所以預設情況下理應返回一個 fulfilled 的新例項。

Promise.prototype.catch() 也會生成一個新的 Promise,其生成規則和 onRejected 是一樣的。

finally生成一個新的Promise

沒想到吧,Promise.prototype.finally() 也能生成一個 Promise。finally 裡的操作是和狀態無關的,一般用來做後續程式碼的處理工作,所以 finally 一般會原樣後傳父 Promise,無論父級例項是什麼狀態。

let p1 = new Promise(() => {}) 
let p2 = p1.finally(() => {}) 
setTimeout(console.log, 0, p2)  // Promise {} 
 
let p3 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p4 = p3.finally(() => {}) 
setTimeout(console.log, 0, p3)  // Promise {: 3}

上面說的是一般,但是也有特殊情況,比如 finally 裡返回了一個非 fulfilled 的 Promise 或者丟擲了異常的時候,則會返回對應狀態的新例項:

let p1 = new Promise((resolve, reject) => { 
    resolve(3) 
}) 
let p2 = p1.finally(() => new Promise(() => {})) 
setTimeout(console.log, 0, p2)  // Promise {} 
 
let p3 = p1.finally(() => Promise.reject(6)) 
setTimeout(console.log, 0, p3)  // Promise {: 6} 
 
let p4 = p1.finally(() => { 
    throw new Error('error') 
}) 
setTimeout(console.log, 0, p4)  // Promise {: Error: error}
執行順序

先來看一段簡單的程式碼:

new Promise((resolve, reject) => { 
    console.log('A') 
    resolve(3) 
    console.log('B') 
}).then(res => { 
    console.log('C') 
}) 
console.log('D') 
// 列印結果:A B D C

上面這串程式碼的輸出順序是:A B D C。從上面章節介紹的知識點我們知道,executor 執行器會在 new Promise 呼叫的時候立即同步執行的,所以先後列印 A B 是沒問題的。當執行 resolve()/reject() 的時候,會將 Promise 對應的處理程式推入微任務佇列,稍等這裡提到的對應的處理程式具體是指什麼?

resolve() 對應 .then() 裡的第一個入參,即 onResolved 函式;
reject() 對應 .then() 裡的第二個入參,即 onRejected 函式;或者 Promise.prototype.catch() 裡的回撥函式;
所以當執行 resolve(3) 的時候(此時下面定義的這個箭頭函式其實就是 onResolved 函式),onResolved 函式將被推入微任務佇列,然後列印 D,此時所有同步任務執行完成,瀏覽器會去檢查微任務佇列,發現存在一個,所以最後會去呼叫 onResolved 函式,列印出 C。

let onResolved = res => { 
    console.log('C') 
}

其實除了 onResolved、onRejected 以及 Promise.prototype.catch() 裡的處理程式外,Promise.prototype.finally() 的處理程式 onFinally 也是非同步執行的:

new Promise((resolve, reject) => { 
    console.log('A') 
    resolve(3) 
}).finally(() => { 
    console.log('B') 
}) 
console.log('C') 
// 列印結果:A C B

Promise 鏈式呼叫的基礎就是因為 onResolved、onRejected、catch() 的處理程式以及 onFinally 會產生一個新的 Promise 例項,且又因為他們都是非同步執行的,所以在鏈式呼叫的時候,對於它們執行順序會稀裡糊塗琢磨不透就是這個原因。

題目一

那下面我們就來看點複雜的例子,先來分析下這章開篇提到的題目:

new Promise((resolve, reject) => { 
    resolve() 
}).then(() => { 
    console.log('A') 
    new Promise((resolve, reject) => { 
        resolve() 
    }).then(() => { 
        console.log('B') 
    }).then(() => { 
        console.log('C') 
    }) 
}).then(() => { 
    console.log('D') 
}) 
// 列印結果:A

為了方便分析,我們把上面的這串程式碼寫得好看一點:

new Promise(executor).then(onResolvedA).then(onResolvedD) 
 
function executor(resolve, reject) { 
    resolve() 
} 
function onResolvedA() { 
    console.log('A') 
    new Promise(executor).then(onResolvedB).then(onResolvedC) 
} 
function onResolvedB() { 
    console.log('B') 
} 
function onResolvedC() { 
    console.log('C') 
} 
function onResolvedD() { 
    console.log('D') 
}

執行過程:

執行 new Promise(),立即同步執行 executor 函式,呼叫 resolve(),此時會將 onResolvedA 推入微任務佇列 1,截止目前所有同步程式碼執行完成;
檢查微任務佇列,執行 onResolvedA 函式,列印 A,執行 new Promise(executor),呼叫 resolve() 函式,此時將 onResolvedB 推入微任務佇列 2;
截止目前微任務佇列 1 的程式碼全部執行完成,即 onResolvedA 函式執行完成。我們知道 onResolved 函式會基於返回值生成一個新的 Promise,而 onResolvedA 函式沒有顯示的返回值,所以其返回值為 undefined,那麼經過 Promise.resolve(undefined) 初始化後會生成一個這樣的新例項:Promise {: undefined};由於這個新的例項狀態已經變成 fulfilled,所以會立即將其處理函式 onResolvedD 推入微任務佇列 3;
開始執行微任務佇列 2 裡的內容,列印 B,同上一條原理,由於 onResolvedB 函式的返回值為 undefined,所以生成了一個 resolved 的新例項,則會立即將 onResolvedC 推入微任務佇列 4;
執行微任務佇列 3,列印 D;
執行微任務佇列 4,列印 C;
至此全部程式碼執行完成,最終的列印結果為:A B D C。

題目二
new Promise((resolve, reject) => { 
    resolve(1) 
}).then(res => { 
    console.log('A') 
}).finally(() => { 
    console.log('B') 
}) 
new Promise((resolve, reject) => { 
    resolve(2) 
}).then(res => { 
    console.log('C') 
}).finally(() => { 
    console.log('D') 
}) 
// 列印結果:A C B D

應該很多人會和我當初一樣好奇:為什麼列印結果不是 A B C D 呢?這裡涉及到一個知識點:如果給 Promise 例項新增了多個處理函式,當例項狀態變化的時候,那麼執行的過程就是按照新增時的順序而執行的。

new Promise((resolve, reject) => { 
    resolve(1) 
}).then(onResolvedA).finally(onFinally) 
 
function onResolvedA() { 
    console.log('A') 
} 
function onFinally() { 
    console.log('B') 
} 
// 列印結果:A B

對於上面這串程式碼,其實 finally() 處理程式執行的時候已經不是透過 new Promise() 初始化的例項,而是執行完 onResolvedA 函式的時候生成的新例項,不信我們將上面程式碼中的函式 onResolvedA 稍微改動下:

new Promise((resolve, reject) => { 
    resolve(1) 
}).then(onResolvedA).finally(onFinally) 
 
function onResolvedA() { 
    console.log('A') 
    return new Promise(() => {}) 
} 
function onFinally() { 
    console.log('B') 
} 
// 列印結果:A

由於 onResolvedA 返回了一個這樣的 Promise { } 新例項,這個新例項的狀態沒有發生變化,所以不會執行 finally 處理程式 onFinally,所以不會列印 B。這個就說明了,鏈式呼叫的時候處理程式的執行是一步一步來的,只要前面的執行完了,生成了新的例項,然後根據新例項的狀態變化,才去執行後續的處理程式。

所以拿最開始那道題來說:

new Promise((resolve, reject) => { 
    resolve(1) 
}).then(res => { 
    console.log('A') 
}).finally(() => { 
    console.log('B') 
}) 
new Promise((resolve, reject) => { 
    resolve(2) 
}).then(res => { 
    console.log('C') 
}).finally(() => { 
    console.log('D') 
}) 
// 列印結果:A C B D

他的執行過程應該是這樣的:

執行 resolve(1),將處理程式 A 推入微任務佇列 1;
執行 resolve(2),將處理程式 C 推入微任務佇列 2;
同步任務執行完成,執行微任務佇列 1 裡的內容,列印 A,A 所在函式執行完成後生成了一個 fulfilled 的新例項,由於新例項狀態變化,所以會立即執行 finally() 處理程式 B 推入微任務佇列 3;
執行微任務佇列 2 的內容,列印 C,C 所在函式執行完成後,同上條原理會將處理程式 D 推入微任務佇列 4;
執行微任務佇列 3 的內容,列印 B;
執行微任務佇列 4 的內容,列印 D;
程式碼全部執行完成,最終列印:A C B D。
題目就先做到這裡,相信你和我一樣,對 Promise 的執行過程應該有更深入的理解了。接下來我們將繼續學習 Promise 的相關 API。

Promise與錯誤處理

平時我們寫程式碼遇到錯誤,都習慣用 try/catch 塊來處理,但是對於 Promise 產生的錯誤,用這個是處理不了的,看下面這段程式碼:

try { 
    new Promise((resolve, reject) => { 
        console.log('A') 
        throw new Error() 
        console.log('B') 
    })   
} catch(err) { 
    console.log(err) 
} 
console.log('C') 
// A 
// C  
// Uncaught (in promise) Error

從執行結果我們可以看到,報錯的資訊出現在列印 C 之後,說明丟擲錯誤這個動作是在非同步任務中做的,所以 catch 捕獲不到該錯誤就在情理之中了,否則就不會列印 C 了。可見,傳統的 try/catch 語句並不能捕獲 Promise 產生的錯誤,而需要使用 onRejected 處理程式:

let p1 = new Promise((resolve, reject) => { 
    console.log('A') 
    throw new Error('error') 
    console.log('B') 
}) 
let p2 = p1.catch((err) => { 
    console.log(err) 
})  
setTimeout(console.log, 0, p2) 
// A 
// Error: error 
// Promise {: undefined}

onRejected 捕獲了上面丟擲的錯誤後,使得程式正常執行,最後還生成了一個 fulfilled的新例項。

除了以上這種直接在 executor 裡透過 throw 主動丟擲一個錯誤外,還可以透過以下方式產出需要 onRejected 處理的錯誤:

new Promise((resolve, reject) => { 
    init() // 被動出錯,呼叫了不存在的函式 
}) 
 
new Promise((resolve, reject) => { 
    reject() 
}) 
 
new Promise((resolve, reject) => { 
    resolve() 
}).then(() => Promise.reject()) 
 
new Promise((resolve, reject) => { 
    resolve() 
}).then(() => { 
    throw new Error() 
})

注意,如果只是產生了一個錯誤,卻沒有丟擲來是不會報錯的:

// 不會報錯 
new Promise((resolve, reject) => { 
    reject() 
}).then(() => new Error())

Promise 出現了錯誤就需要使用 onRejected 處理程式處理,否則程式就會報錯,執行不下去了。

Promise API
Promise.resolve()

並非所有的 Promise 的初始狀態都是 pending,可以透過 Promise.resolve(value) 來初始化一個狀態為 fulfilled,值為 value 的 Promise 例項:

let p = Promise.resolve(3) 
console.log(p)  // Promise {: 3}

這個操作和下面這種建立一個 fulfilled 的 Promise 在效果上是一樣的:

let p = new Promise(resolve => resolve(3)) 
console.log(p)  // Promise {: 3}

使用這個靜態方法,理論上可以把任何一個值轉換成 Promise:

setTimeout(console.log, 0, Promise.resolve())  // Promise {: undefined} 
setTimeout(console.log, 0, Promise.resolve(3, 6, 9))  // Promise {: 3} 多餘的引數將被忽略 
setTimeout(console.log, 0, Promise.resolve(new Error('error')))  // Promise {: Error: error}

這個被轉換的值甚至可以是一個 Promise 物件,如果是這樣,Promise.resolve 會將其原樣輸出:

let p = Promise.resolve(3) 
setTimeout(console.log, 0, p === Promise.resolve(p))  // true
Promise.reject()

和 Promise.resolve() 類似,Promise.reject() 會例項化一個 rejected 狀態的 Promise,且會丟擲一個錯誤,該錯誤只能透過拒絕處理程式捕獲。

Promise 
    .reject(3) 
    .catch(err => { 
        console.log(err)  // 3 
    })

對於初始化一個 rejected 狀態的例項,以下兩種寫法都可以達到這個目的:

let p1 = Promise.reject() 
let p2 = new Promise((resolve, reject) => reject())

與 Promise.resolve() 不同的是,如果給 Promise.reject() 傳遞一個 Promise 物件,則這個物件會成為新 Promise 的值:

let p = Promise.reject(3) 
setTimeout(console.log, 0, p === Promise.reject(p))  // false
Promise.all()

Promise.all(iterable) 用來將多個 Promise 例項合成一個新例項。引數必須是一個可迭代物件,通常是陣列。

Promise.all([ 
    Promise.resolve(3), 
    Promise.resolve(6) 
])

可迭代物件裡的所有元素都會透過 Promise.resolve() 轉成 Promise:

Promise.all([3, 6, 9])

所有 Promise 都 resolve 後,Promise.all() 才會生成一個 fulfilled 的新例項。且新例項的內部值是由所有 Promise 解決後的值組成的陣列:

let p1 = Promise.all([ 
    Promise.resolve('3'), 
    Promise.resolve(), 
    6 
]) 
let p2 = p1.then(res => { 
    console.log(res) 
}) 
setTimeout(console.log, 0, p1) 
// ["3", undefined, 6] 
// Promise {: Array(3)}

所有 Promise 中,只要出現一個 pending 狀態的例項,那麼合成的新例項也是 pending 狀態的:

let p1 = Promise.all([ 
    3, 
    Promise.resolve(6), 
    new Promise(() => {}) 
]) 
setTimeout(console.log, 0, p1) 
// Promise {}

所有 Promise 中,只要出現一個 rejected 狀態的例項,那麼合成的新例項也是 rejected狀態的,且新例項的內部值是第一個拒絕 Promise 的內部值:

let p1 = Promise.all([ 
    3, 
    Promise.reject(6), 
    new Promise((resolve, reject) => { 
        reject(9) 
    }) 
]) 
let p2 = p1.catch(err => { 
    console.log(err) 
}) 
setTimeout(console.log, 0, p1) 
// 6 
// Promise {: 6}
Promise.race()

Promise.race(iterable) 會返回一個由所有可迭代例項中第一個 fulfilled 或 rejected的例項包裝後的新例項。

let p1 = Promise.race([ 
    3, 
    Promise.reject(6), 
    new Promise((resolve, reject) => { 
        resolve(9) 
    }).then(res => { 
        console.log(res) 
    }) 
]) 
let p2 = p1.then(res => { 
    console.log(err) 
}) 
setTimeout(console.log, 0, p1) 
// 9 
// 3 
// Promise {: 3}

來將上面這串程式碼變動下:

function init(){ 
    console.log(3) 
    return 3 
} 
let p1 = Promise.race([ 
    new Promise((resolve, reject) => { 
        resolve(9) 
    }).then(res => { 
        console.log(res) 
        return 'A' 
    }), 
    new Promise((resolve, reject) => { 
        reject(6) 
    }), 
    init(), 
]) 
let p2 = p1.then(res => { 
    console.log(res) 
}, err => { 
    console.log(err) 
}) 
setTimeout(console.log, 0, p1) 
// 3 
// 9 
// 6 
// Promise {: 6}

想要知道 Promise.race() 的結果,無非是要知道到底誰才是第一個狀態變化的例項,讓我們來具體分析下程式碼執行過程:

迭代第一個元素,執行同步程式碼 resolve(9),由 new Promise 初始化的例項的狀態已經變為了 fulfilled,所以第一個狀態變化的例項已經出現了嗎?其實並沒有,因為迭代第一個元素的程式碼還沒執行完成呢,然後會將 return 'A' 所在函式的這段處理程式推入微任務佇列 1;
迭代第二個元素,執行 reject(6),所以由 new Promise 初始化的例項的狀態已經變為 rejected,由於該例項沒有處理函式,所以迭代第二個元素的程式碼已經全部執行完成,此時,第一個狀態變化的例項已經產生;
迭代第三個元素,是一個函式,執行同步程式碼列印出 3,然後用 Promise.resolve 將函式返回值 3 轉成一個 Promise {: 3} 的新例項,這是第二個狀態發生變化的例項;
此時所有迭代物件遍歷完成,即同步程式碼執行完成,開始執行微任務佇列 1 的內容,列印 res,其值是 9,然後處理程式返回了 'A',此時根據之前提到的知識點,這裡會新生成一個 Promise {: 'A'} 的例項,這是第三個狀態發生變化的例項。此時,第一個迭代元素的程式碼已經全部執行完成,所以第一個迭代元素最終生成的例項是第三次狀態發生變化的這個;
此時 p1 已經產生,它是 Promise {: 6},所以會將它的處理程式 console.log(err) 所在函式推入微任務佇列 2;
執行微任務佇列 2 的內容,列印 err,其值是 6;
所有微任務執行完成,開始執行 setTimeout 裡的宏任務,列印 p1,至此全部程式碼執行完成。

Promise.allSettled()

Promise.allSettled(iterable) 當所有的例項都已經 settled,即狀態變化過了,那麼將返回一個新例項,該新例項的內部值是由所有例項的值和狀態組合成的陣列,陣列的每項是由每個例項的狀態和值組成的物件。

function init(){ 
    return 3 
} 
let p1 = Promise.allSettled([ 
    new Promise((resolve, reject) => { 
        resolve(9) 
    }).then(res => {}), 
    new Promise((resolve, reject) => { 
        reject(6) 
    }), 
    init() 
]) 
let p2 = p1.then(res => { 
    console.log(res) 
}, err => { 
    console.log(err) 
}) 
// [ 
//      {status: "fulfilled", value: undefined},  
//      {status: "rejected", reason: 6},  
//      {status: "fulfilled", value: 3} 
// ]

只要所有例項中包含一個 pending 狀態的例項,那麼 Promise.allSettled() 的結果為返回一個這樣 Promise { } 的例項。

❝Promise.allSettled() 是 ES2020 中新增的方法,所以有一些瀏覽器可能還暫時不支援。❞

對於不支援的瀏覽器,可以寫 polyfill:

if(!Promise.allSettled) { 
    Promise.allSettled = function(promises) { 
        return Promise.all(promises.map(p => Promise.resolve(p) 
            .then(value => ({ 
                status: 'fulfilled', 
                value 
            }), reason => ({ 
                status: 'rejected', 
                reason 
            })) 
        )); 
    } 
}

感謝閱讀

首先感謝你閱讀本文,相信你付出的時間值得擁有這份回報。

原文地址:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559985/viewspace-2740743/,如需轉載,請註明出處,否則將追究法律責任。

相關文章