【JS】779- 深入理解Promise
這是布蘭的第 7 篇原創(chuàng)
從異步編程說起
我們都知道 JavaScript 的代碼執(zhí)行的時候是跑在單線程上的,可以理解為只能按照代碼的出現(xiàn)順序,從上到下一行一行的執(zhí)行,但是遇到了異步的行為,比如定時器(一定時間之后才去執(zhí)行),那就需要等同步代碼執(zhí)行完成后的一段時間里再去執(zhí)行異步代碼。
對于同步行為,如下面的代碼,我們能夠很清楚的知道每一行會發(fā)生什么,這是因為后面的指令總是等到前面的指令執(zhí)行完成后才去執(zhí)行,所以這里的第二行里的變量 x 在內存里已經是定義過的。
let?x?=?10;
let?y?=?x?+?5;
但是對于異步代碼,我們就不好推斷到底什么時候會執(zhí)行完成了。比如舉一個實際的例子,我們去動態(tài)加載某個腳本,會這樣做:
function?loadScript(src)?{
????let?script?=?document.createElement('script')
????script.src?=?src
????document.head.append(script)
}
這個腳本加載完成的時候會去執(zhí)行定義在腳本里的一些函數(shù),比如初始化函數(shù) init,那么我們可以會這樣寫:
function?loadScript(src)?{
????let?script?=?document.createElement('script')
????script.src?=?src
????document.head.append(script)
}
loadScript('./js/script.js')
init()??//?定義在?./js/script.js?里的函數(shù)
但是實際執(zhí)行后卻發(fā)現(xiàn),這樣根本不行,因為加載腳本是需要花時間的,是一個異步的行為,瀏覽器執(zhí)行 JavaScript 的時候并不會等到腳本加載完成的時候再去調用 init 函數(shù)。
以往,對于這種異步編程的做法通常就是通過給函數(shù)傳遞一個回調函數(shù)來處理,上面那個例子可以這樣做:
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?中的函數(shù)
}
function?fail()?{
????console.log('fail')
}
上面這樣做能夠保證在腳本加載完成的時候,再去執(zhí)行腳本里的函數(shù)。但是多考慮一個問題,如果 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 函數(shù)將多層嵌套的代碼改成了同步處理流程,看起來效果還是不錯的,那什么是 Promise 呢?
Promise 首先是一個對象,它通常用于描述現(xiàn)在開始執(zhí)行,一段時間后才能獲得結果的行為(異步行為),內部保存了該異步行為的結果。然后,它還是一個有狀態(tài)的對象:
pending:待定fulfilled:兌現(xiàn),有時候也叫解決(resolved)rejected:拒絕
一個 Promise 只有這 3 種狀態(tài),且狀態(tài)的轉換過程有且僅有 2 種:
pending到fulfilledpending到rejected
可以通過如下的 Promise 對象構造器來創(chuàng)建一個 Promise:
let?promise?=?new?Promise((resolve,?reject)?=>?{})
傳遞給 new Promise 的是 executor 執(zhí)行器。當 Promise 被創(chuàng)建的時候,executor 會立即同步執(zhí)行。executor 函數(shù)里通常做了 2 件事情:初始化一個異步行為和控制狀態(tài)的最終轉換。
new?Promise((resolve,?reject)?=>?{
????setTimeout(()?=>?{
????????resolve()
????},?1000)
})
如上代碼所示,setTimeout 函數(shù)用來描述一個異步行為,而 resolve 用來改變狀態(tài)。executor 函數(shù)包含 2 個參數(shù),他們都是回調函數(shù),用于控制 Promise 的狀態(tài)轉換:
resolve:用來將狀態(tài)pending轉換成fulfilledreject:用來將狀態(tài)pending轉換成rejected
一個 Promise 的狀態(tài)一旦被轉換過,則無法再變更:
let?p?=?new?Promise((resolve,?reject)?=>?{
????setTimeout(()?=>?{
????????resolve('第一次?resolve')
????????resolve('第二次?resolve')??//?將被忽略
????????reject('第一次?reject')??//?將被忽略
????},?0)
})
setTimeout(console.log,?1000,?p)??//?Promise?{:?"第一次?resolve"}
可以看到執(zhí)行了 2 次 resolve 函數(shù)和 1 次 reject 函數(shù),但是 promise 的最終結果是取的第一次 resolve 的結果,印證了上面的結論。
由 new Promise 構造器返回的 Promise 對象具有如下內部屬性:
PromiseState:最初是pending,resolve被調用的時候變?yōu)?fulfilled,或者reject被調用時會變?yōu)?rejected;PromiseResult:最初是undefined,resolve(value)被調用時變?yōu)?value,或者在reject(error)被調用時變?yōu)?error。
比如上面例子中打印出來的 Promise 對象結果中,fulfilled 是其內部的 PromiseState,而 “第一次 resolve” 是其 PromiseResult。
//?Promise?{:?"第一次?resolve"}
Promise實例方法
Promise.prototype.then()
Promise.prototype.then() 將用于為 Promise 實例添加處理程序的函數(shù)。它接受 2 個可選的參數(shù):
onResolved:狀態(tài)由pending轉換成fulfilled時執(zhí)行;onRejected:狀態(tài)由pending轉換成rejected時執(zhí)行。
它可以寫成這樣:
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)
})
因為狀態(tài)的變化只有 2 種,所以 onResolved 和 onRejected 在執(zhí)行的時候必定是互斥。
上面介紹到了 then() 的參數(shù)是可選的,當只有 onResolved 的時候可以這樣寫:
new?Promise((resolve,?reject)?=>?{
????resolve()
}).then(res?=>?{})
當參數(shù)只有 onRejected 的時候,需要把第一個參數(shù)設置為 null:
new?Promise((resolve,?reject)?=>?{
????reject()
}).then(null,?err?=>?{})
如果給 then() 函數(shù)傳遞來了非函數(shù)參數(shù),則會默認忽略。
Promise.prototype.catch()
Promise.prototype.catch() 用于給 Promise 對象添加拒絕處理程序。只接受一個參數(shù):onRejected 函數(shù)。實際上,下面這兩種寫法是等效的:
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 函數(shù),這個函數(shù)主要是做一些清理的工作,只有狀態(tài)變化的時候才會執(zhí)行該 onFinally 函數(shù)。
function?onFinally()?{
????console.log(888)??//?并不會執(zhí)行??
}
new?Promise((resolve,?reject)?=>?{
????
}).finally(onFinally)
因為 onFinally 函數(shù)是沒有任何參數(shù)的,所以在其內部其實并不知道該 Promise 的狀態(tài)是怎么樣的。
鏈式調用
鏈式調用里涉及到的知識點很多,我們不妨先看看下面這道題,你能正確輸出其打印順序嘛?
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 函數(shù)返回了一個 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 狀態(tài)的 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() 的時候,狀態(tài)由 pending 變成 rejected,從而調用 onRejected 進行處理,但是為什么有時候會返回一個 fulfilled 的新實例呢?試著想一下,如果 onRejected 返回了一個 pending 的或者 rejected 狀態(tài)的新實例,那后續(xù)的鏈式調用就進行不下去了,看下面例子:
new?Promise((resolve,?reject)?=>?{
????reject()
}).then(null,?()?=>?{
????console.log('A')
}).then(()?=>?{
????console.log('B')
}).then(()?=>?{
????console.log('C')
}).catch(()?=>?{
????console.log('D')
})
如果 A 處理函數(shù)這里返回了一個 pending 狀態(tài)的新實例,那么后續(xù)所有的鏈式操作都無法執(zhí)行;或者返回的是一個 rejected 狀態(tài)的新實例,那么后續(xù)的 B 和 C 也就無法執(zhí)行了,那居然都不能執(zhí)行 B 和 C 所在處理程序,那定義來干嘛呢?鏈式操作就毫無鏈式可言。又,onRejected 的存在的根本意義無非就是用于捕獲 Promise 產生的錯誤,從而不影響程序的正常執(zhí)行,所以默認情況下理應返回一個 fulfilled 的新實例。
Promise.prototype.catch() 也會生成一個新的 Promise,其生成規(guī)則和 onRejected 是一樣的。
finally生成一個新的Promise
沒想到吧,Promise.prototype.finally() 也能生成一個 Promise。finally 里的操作是和狀態(tài)無關的,一般用來做后續(xù)代碼的處理工作,所以 finally 一般會原樣后傳父 Promise,無論父級實例是什么狀態(tài)。
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 或者拋出了異常的時候,則會返回對應狀態(tài)的新實例:
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}
執(zhí)行順序
先來看一段簡單的代碼:
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。從上面章節(jié)介紹的知識點我們知道,executor 執(zhí)行器會在 new Promise 調用的時候立即同步執(zhí)行的,所以先后打印 A B 是沒問題的。當執(zhí)行 resolve()/reject() 的時候,會將 Promise 對應的處理程序推入微任務隊列,稍等這里提到的對應的處理程序具體是指什么?
resolve()對應.then()里的第一個入參,即onResolved函數(shù);reject()對應.then()里的第二個入參,即onRejected函數(shù);或者Promise.prototype.catch()里的回調函數(shù);
所以當執(zhí)行 resolve(3) 的時候(此時下面定義的這個箭頭函數(shù)其實就是 onResolved 函數(shù)),onResolved 函數(shù)將被推入微任務隊列,然后打印 D,此時所有同步任務執(zhí)行完成,瀏覽器會去檢查微任務隊列,發(fā)現(xiàn)存在一個,所以最后會去調用 onResolved 函數(shù),打印出 C。
let?onResolved?=?res?=>?{
????console.log('C')
}
其實除了 onResolved、onRejected 以及 Promise.prototype.catch() 里的處理程序外,Promise.prototype.finally() 的處理程序 onFinally 也是異步執(zhí)行的:
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 實例,且又因為他們都是異步執(zhí)行的,所以在鏈式調用的時候,對于它們執(zhí)行順序會稀里糊涂琢磨不透就是這個原因。
題目一
那下面我們就來看點復雜的例子,先來分析下這章開篇提到的題目:
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')
}
執(zhí)行過程:
執(zhí)行 new Promise(),立即同步執(zhí)行executor函數(shù),調用resolve(),此時會將onResolvedA推入微任務隊列 1,截止目前所有同步代碼執(zhí)行完成;檢查微任務隊列,執(zhí)行 onResolvedA函數(shù),打印 A,執(zhí)行new Promise(executor),調用resolve()函數(shù),此時將onResolvedB推入微任務隊列 2;截止目前微任務隊列 1 的代碼全部執(zhí)行完成,即 onResolvedA函數(shù)執(zhí)行完成。我們知道onResolved函數(shù)會基于返回值生成一個新的Promise,而onResolvedA函數(shù)沒有顯示的返回值,所以其返回值為undefined,那么經過Promise.resolve(undefined)初始化后會生成一個這樣的新實例:Promise {;由于這個新的實例狀態(tài)已經變成: undefined} fulfilled,所以會立即將其處理函數(shù)onResolvedD推入微任務隊列 3;開始執(zhí)行微任務隊列 2 里的內容,打印 B,同上一條原理,由于 onResolvedB函數(shù)的返回值為undefined,所以生成了一個resolved的新實例,則會立即將onResolvedC推入微任務隊列 4;執(zhí)行微任務隊列 3,打印 D; 執(zhí)行微任務隊列 4,打印 C; 至此全部代碼執(zhí)行完成,最終的打印結果為: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 實例添加了多個處理函數(shù),當實例狀態(tài)變化的時候,那么執(zhí)行的過程就是按照添加時的順序而執(zhí)行的。
new?Promise((resolve,?reject)?=>?{
????resolve(1)
}).then(onResolvedA).finally(onFinally)
function?onResolvedA()?{
????console.log('A')
}
function?onFinally()?{
????console.log('B')
}
//?打印結果:A B
對于上面這串代碼,其實 finally() 處理程序執(zhí)行的時候已經不是通過 new Promise() 初始化的實例,而是執(zhí)行完 onResolvedA 函數(shù)的時候生成的新實例,不信我們將上面代碼中的函數(shù) 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 { 新實例,這個新實例的狀態(tài)沒有發(fā)生變化,所以不會執(zhí)行 finally 處理程序 onFinally,所以不會打印 B。這個就說明了,鏈式調用的時候處理程序的執(zhí)行是一步一步來的,只要前面的執(zhí)行完了,生成了新的實例,然后根據(jù)新實例的狀態(tài)變化,才去執(zhí)行后續(xù)的處理程序。
所以拿最開始那道題來說:
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
他的執(zhí)行過程應該是這樣的:
執(zhí)行 resolve(1),將處理程序 A 推入微任務隊列 1;執(zhí)行 resolve(2),將處理程序 C 推入微任務隊列 2;同步任務執(zhí)行完成,執(zhí)行微任務隊列 1 里的內容,打印 A,A 所在函數(shù)執(zhí)行完成后生成了一個 fulfilled的新實例,由于新實例狀態(tài)變化,所以會立即執(zhí)行finally()處理程序 B 推入微任務隊列 3;執(zhí)行微任務隊列 2 的內容,打印 C,C 所在函數(shù)執(zhí)行完成后,同上條原理會將處理程序 D 推入微任務隊列 4; 執(zhí)行微任務隊列 3 的內容,打印 B; 執(zhí)行微任務隊列 4 的內容,打印 D; 代碼全部執(zhí)行完成,最終打印:A C B D。
題目就先做到這里,相信你和我一樣,對 Promise 的執(zhí)行過程應該有更深入的理解了。接下來我們將繼續(xù)學習 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
從執(zhí)行結果我們可以看到,報錯的信息出現(xiàn)在打印 C 之后,說明拋出錯誤這個動作是在異步任務中做的,所以 catch 捕獲不到該錯誤就在情理之中了,否則就不會打印 C 了。可見,傳統(tǒng)的 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 捕獲了上面拋出的錯誤后,使得程序正常執(zhí)行,最后還生成了一個 fulfilled 的新實例。
除了以上這種直接在 executor 里通過 throw 主動拋出一個錯誤外,還可以通過以下方式產出需要 onRejected 處理的錯誤:
new?Promise((resolve,?reject)?=>?{
????init()?//?被動出錯,調用了不存在的函數(shù)
})
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 出現(xiàn)了錯誤就需要使用 onRejected 處理程序處理,否則程序就會報錯,執(zhí)行不下去了。
Promise API
Promise.resolve()
并非所有的 Promise 的初始狀態(tài)都是 pending,可以通過 Promise.resolve(value) 來初始化一個狀態(tài)為 fulfilled,值為 value 的 Promise 實例:
let?p?=?Promise.resolve(3)
console.log(p)??//?Promise?{:?3}
這個操作和下面這種創(chuàng)建一個 fulfilled 的 Promise 在效果上是一樣的:
let?p?=?new?Promise(resolve?=>?resolve(3))
console.log(p)??//?Promise?{:?3}
使用這個靜態(tài)方法,理論上可以把任何一個值轉換成 Promise:
setTimeout(console.log,?0,?Promise.resolve())??//?Promise?{:?undefined}
setTimeout(console.log,?0,?Promise.resolve(3,?6,?9))??//?Promise?{:?3}?多余的參數(shù)將被忽略
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 狀態(tài)的 Promise,且會拋出一個錯誤,該錯誤只能通過拒絕處理程序捕獲。
Promise
????.reject(3)
????.catch(err?=>?{
????????console.log(err)??//?3
????})
對于初始化一個 rejected 狀態(tài)的實例,以下兩種寫法都可以達到這個目的:
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 實例合成一個新實例。參數(shù)必須是一個可迭代對象,通常是數(shù)組。
Promise.all([
????Promise.resolve(3),
????Promise.resolve(6)
])
可迭代對象里的所有元素都會通過 Promise.resolve() 轉成 Promise:
Promise.all([3,?6,?9])
所有 Promise 都 resolve 后,Promise.all() 才會生成一個 fulfilled 的新實例。且新實例的內部值是由所有 Promise 解決后的值組成的數(shù)組:
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 中,只要出現(xiàn)一個 pending 狀態(tài)的實例,那么合成的新實例也是 pending 狀態(tài)的:
let?p1?=?Promise.all([
????3,
????Promise.resolve(6),
????new?Promise(()?=>?{})
])
setTimeout(console.log,?0,?p1)
//?Promise?{}
所有 Promise 中,只要出現(xiàn)一個 rejected 狀態(tài)的實例,那么合成的新實例也是 rejected 狀態(tài)的,且新實例的內部值是第一個拒絕 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() 的結果,無非是要知道到底誰才是第一個狀態(tài)變化的實例,讓我們來具體分析下代碼執(zhí)行過程:
迭代第一個元素,執(zhí)行同步代碼 resolve(9),由new Promise初始化的實例的狀態(tài)已經變?yōu)榱?fulfilled,所以第一個狀態(tài)變化的實例已經出現(xiàn)了嗎?其實并沒有,因為迭代第一個元素的代碼還沒執(zhí)行完成呢,然后會將return 'A'所在函數(shù)的這段處理程序推入微任務隊列 1;迭代第二個元素,執(zhí)行 reject(6),所以由new Promise初始化的實例的狀態(tài)已經變?yōu)?rejected,由于該實例沒有處理函數(shù),所以迭代第二個元素的代碼已經全部執(zhí)行完成,此時,第一個狀態(tài)變化的實例已經產生;迭代第三個元素,是一個函數(shù),執(zhí)行同步代碼打印出 3,然后用 Promise.resolve將函數(shù)返回值 3 轉成一個Promise {的新實例,這是第二個狀態(tài)發(fā)生變化的實例;: 3} 此時所有迭代對象遍歷完成,即同步代碼執(zhí)行完成,開始執(zhí)行微任務隊列 1 的內容,打印 res,其值是 9,然后處理程序返回了 'A',此時根據(jù)之前提到的知識點,這里會新生成一個Promise {的實例,這是第三個狀態(tài)發(fā)生變化的實例。此時,第一個迭代元素的代碼已經全部執(zhí)行完成,所以第一個迭代元素最終生成的實例是第三次狀態(tài)發(fā)生變化的這個;: 'A'} 此時 p1已經產生,它是Promise {,所以會將它的處理程序: 6} console.log(err)所在函數(shù)推入微任務隊列 2;執(zhí)行微任務隊列 2 的內容,打印 err,其值是 6;所有微任務執(zhí)行完成,開始執(zhí)行 setTimeout里的宏任務,打印p1,至此全部代碼執(zhí)行完成。
Promise.allSettled()
Promise.allSettled(iterable) 當所有的實例都已經 settled,即狀態(tài)變化過了,那么將返回一個新實例,該新實例的內部值是由所有實例的值和狀態(tài)組合成的數(shù)組,數(shù)組的每項是由每個實例的狀態(tài)和值組成的對象。
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 狀態(tài)的實例,那么 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
????????????}))
????????));
????}
}

回復“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
