文章轉載自掘金:Promise使用手冊
本篇以Promise爲核心, 逐步展開, 最終分析process.nextTick , promise.then , setTimeout , setImmediate 它們的異步機制.
導讀
Promise問世已久, 其科普類文章亦不計其數. 遂本篇初衷不爲科普, 只爲能夠溫故而知新.
比如說, catch能捕獲所有的錯誤嗎? 爲什麼有些時候會拋出”Uncaught (in promise) …”? Promise.resolve
和 Promise.reject
處理Promise對象時又有什麼不一樣的地方?
Promise
引子
閱讀此篇之前, 我們先體驗一下如下代碼:
setTimeout(function() {
console.log(4)
}, 0);
new Promise(function(resolve) {
console.log(1);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve()
}
console.log(2);
}).then(function() {
console.log(5)
});
console.log(3);
這裏先賣個關子, 後續將給出答案並提供詳細分析.
和往常文章一樣, 我喜歡從api入手, 先具象地瞭解一個概念, 然後再抽象或擴展這個概念, 接着再談談概念的具體應用場景, 通常末尾還會有一個簡短的小結. 這樣, 查詢api的讀者可以選擇性地閱讀上文, 希望深入的讀者可以繼續剖析概念, 當然我更希望你能耐心地讀到應用場景處, 這樣便能昇華對這個概念或技術的運用, 也能避免踩坑.
new Promise
Promise的設計初衷是避免異步回調地獄. 它提供更簡潔的api, 同時展平回調爲鏈式調用, 使得代碼更加清爽, 易讀.
如下, 即創建一個Promise對象:
const p = new Promise(function(resolve, reject) {
console.log('Create a new Promise.');
});
console.log(p);
new Promise
創建Promise時, 瀏覽器同步執行傳入的第一個方法, 從而輸出log. 新創建的promise實例對象, 初始狀態爲等待(pending), 除此之外, Promise還有另外兩個狀態:
- fulfilled, 表示操作完成, 實現了. 只在resolve方法執行時才進入該狀態.
- rejected, 表示操作失敗, 拒絕了. 只在reject方法執行時或拋出錯誤的情況下才進入該狀態.
如下圖展示了Promise的狀態變化過程(圖片來自MDN):
從初始狀態(pending)到實現(fulfilled)或拒絕(rejected)狀態的轉換, 這是兩個分支, 實現或拒絕即最終狀態, 一旦到達其中之一的狀態, promise的狀態便穩定了. (因此, 不要嘗試實現或拒絕狀態的互轉, 它們都是最終狀態, 沒法轉換)
以上, 創建Promise對象時, 傳入的回調函數function(resolve, reject){}默認擁有兩個參數, 分別爲:
- resolve, 用於改變該Promise本身的狀態爲實現, 執行後, 將觸發then的onFulfilled回調, 並把resolve的參數傳遞給onFulfilled回調.
- reject, 用於改變該Promise本身的狀態爲拒絕, 執行後, 將觸發 then | catch的onRejected回調, 並把reject的參數傳遞給onRejected回調.
Promise的原型僅有兩個自身方法, 分別爲 Promise.prototype.then
, Promise.prototype.catch
. 而它自身僅有四個方法, 分別爲 Promise.reject
, Promise.resolve
, Promise.all
, Promise.race
.
then
語法: Promise.prototype.then(onFulfilled, onRejected)
用於綁定後續操作. 使用十分簡單:
p.then(function(res) {
console.log('此處執行後續操作');
});
// 當然, then的最大便利之處便是可以鏈式調用
p.then(function(res) {
console.log('先做一件事');
}).then(function(res) {
console.log('再做一件事');
});
// then還可以同時接兩個回調,分別處理成功和失敗狀態
p.then(function(SuccessRes) {
console.log('處理成功的操作');
}, function(failRes) {
console.log('處理失敗的操作');
});
不僅如此, Promise的then中還可返回一個新的Promise對象, 後續的then將接着繼續處理這個新的Promise對象.
p.then(function(){
return new Promise(function(resolve, reject) {
console.log('這裏是一個新的Promise對象');
resolve('New Promise resolve.');
});
}).then(function(res) {
console.log(res);
});
那麼, 如果沒有指定返回值, 會怎麼樣?
根據Promise規範, then或catch即使未顯式指定返回值, 它們也總是默認返回一個新的fulfilled狀態的promise對象.(筆者注:這也意味着除非Promise內部發生不可捕獲的錯誤,否則Promise最終狀態總是{[[PromiseStatus]]: "resolved"
。在Chrome下輸出“resolved”等同於“fulfilled”)
catch
語法: Promise.prototype.catch(onRejected)
用於捕獲並處理異常.無論是程序拋出的異常, 還是主動reject掉Promise自身, 都會被catch捕獲到.
new Promise(function(resolve, reject) {
reject('該prormise已被拒絕');
}).catch(function(reason) {
console.log('catch:', reason);
});
同then語句一樣, catch也是可以鏈式調用的.
new Promise(function(resolve, reject){
reject('該prormise已被拒絕');
}).catch(function(reason){
console.log('catch:', reason);
console.log(a);
}).catch(function(reason){
console.log(reason);
});
以上, 將依次輸出兩次log, 第一次輸出”promise被拒絕”, 第二次輸出”ReferenceError a is not defined”的堆棧信息.
catch能捕獲哪些錯誤
那是不是catch可以捕獲所有錯誤呢? 可以, 怎麼不可以, 我以前也這麼天真的認爲. 直到有一天我執行了如下的語句, 我就學乖了.
new Promise(function(resolve, reject){
Promise.reject('返回一個拒絕狀態的Promise');
}).catch(function(reason){
console.log('catch:', reason);
});
執行結果如下:
爲什麼catch沒有捕獲到該錯誤呢? 這個問題, 待下一節我們瞭解了Promise.reject語法後再做分析.
Promise.reject
語法: Promise.reject(value)
該方法返回一個拒絕狀態的Promise對象, 同時傳入的參數作爲PromiseValue.
//params: String
Promise.reject('該prormise已被拒絕').catch(function(reason){
console.log('catch:', reason);
});
//params: Error
Promise.reject(new Error('這是一個error')).then(function(res) {
console.log('fulfilled:', res);
}, function(reason) {
console.log('rejected:', reason); // rejected: Error: 這是一個error...
});
即使參數爲Promise對象, 它也一樣會把Promise當作拒絕的理由, 在外部再包裝一個拒絕狀態的Promise對象予以返回.
//params: Promise
const p = new Promise(function(resolve) {
console.log('This is a promise');
});
Promise.reject(p).catch(function(reason) {
console.log('rejected:', reason);
console.log(p == reason);
});
// "This is a promise"
// rejected: Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
// true
以上代碼片段, Promise.reject(p) 進入到了catch語句中, 說明其返回了一個拒絕狀態的Promise, 同時拒絕的理由就是傳入的參數p.
錯誤處理
我們都知道, Promise.reject返回了一個拒絕狀態的Promise對象. 對於這樣的Promise對象, 如果其後續then | catch中都沒有聲明onRejected回調, 它將會拋出一個 “Uncaught (in promise) …”的錯誤. 如上圖所示, 原語句是 “Promise.reject(‘返回一個拒絕狀態的Promise’);” 其後續並沒有跟隨任何then | catch語句, 因此它將拋出錯誤, 且該錯外部的Promise無法捕獲.
不僅如此, Promise之間涇渭分明, 內部Promise拋出的任何錯誤, 外部Promise對象都無法感知並捕獲. 同時, 由於promise是異步的, try catch語句也無法捕獲其錯誤.
因此養成良好習慣, promise記得寫上catch.
除了catch, nodejs下Promise拋出的錯誤, 還會被進程的unhandledRejection
和 rejectionHandled
事件捕獲.
var p = new Promise(function(resolve, reject){
//console.log(a);
reject('rejected');
});
setTimeout(function(){
p.catch(function(reason){
console.info('promise catch:', reason);
});
});
process.on('uncaughtException', (e) => {
console.error('uncaughtException', e);
});
process.on('unhandledRejection', (e) => {
console.info('unhandledRejection:', e);
});
process.on('rejectionHandled', (e) => {
console.info('rejectionHandled', e);
});
//unhandledRejection: rejected
//rejectionHandled Promise { <rejected> 'rejected' }
//promise catch: rejected
即使去掉以上代碼中的註釋, 輸出依然一致. 可見, Promise內部拋出的錯誤, 都不會被uncaughtException事件捕獲.
鏈式寫法的好處
請看如下代碼:
new Promise(function(resolve, reject) {
resolve('New Promise resolve.');
}).then(function(str) {
throw new Error("oops...");
},function(error) {
console.log('then catch:', error);
}).catch(function(reason) {
console.log('catch:', reason);
});
//catch: Error: oops...
可見, then語句的onRejected回調並不能捕獲onFulfilled回調內拋出的錯誤, 尾隨其後的catch語句卻可以, 因此推薦鏈式寫法.
Promise.resolve
語法: Promise.resolve(value | promise | thenable)
thenable 表示一個定義了 then 方法的對象或函數.
參數爲promise時, 返回promise本身.
參數爲thenable的對象或函數時, 將其then屬性作爲new promise時的回調, 返回一個包裝的promise對象.(注意: 這裏與Promise.reject直接包裝一個拒絕狀態的Promise不同)
其他情況下, 返回一個實現狀態的Promise對象, 同時傳入的參數作爲PromiseValue.
//params: String
//return: fulfilled Promise
Promise.resolve('返回一個fulfilled狀態的promise').then(function(res) {
console.log(res); // "返回一個fulfilled狀態的promise"
});
//params: Array
//return: fulfilled Promise
Promise.resolve(['a', 'b', 'c']).then(function(res) {
console.log(res); // ["a", "b", "c"]
});
//params: Promise
//return: Promise self
let resolveFn;
const p2 = new Promise(function(resolve) {
resolveFn = resolve;
});
const r2 = Promise.resolve(p2);
r2.then(function(res) {
console.log(res);
});
resolveFn('xyz'); // "xyz"
console.log(r2 === p2); // true
//params: thenable Object
//return: 根據thenable的最終狀態返回不同的promise
const thenable = {
then: function(resolve, reject) { //作爲new promise時的回調函數
reject('promise rejected!');
}
};
Promise.resolve(thenable).then(function(res) {
console.log('res:', res);
}, function(reason) {
console.log('reason:', reason);
});
可見, Promise.resolve並非返回實現狀態的Promise這麼簡單, 我們還需基於傳入的參數動態判斷.
至此, 我們基本上不用期望使用Promise全局方法中去改變其某個實例的狀態.
- 對於Promise.reject(promise), 它只是簡單地包了一個拒絕狀態的promise殼, 參數promise什麼都沒變.
- 對於Promise.resolve(promise), 僅僅返回參數promise本身.
Promise.all
語法: Promise.all(iterable)
該方法接一個迭代器(如數組等), 返回一個新的Promise對象. 如果迭代器中所有的Promise對象都被實現, 那麼, 返回的Promise對象狀態爲”fulfilled”, 反之則爲”rejected”. 概念上類似Array.prototype.every.
//params: all fulfilled promise
//return: fulfilled promise
Promise.all([1, 2, 3]).then(function(res){
console.log('promise fulfilled:', res); // promise fulfilled: [1, 2, 3]
});
//params: has rejected promise
//return: rejected promise
const p = new Promise(function(resolve, reject){
reject('rejected');
});
Promise.all([1, 2, p]).then(function(res){
console.log('promise fulfilled:', res);
}).catch(function(reason){
console.log('promise reject:', reason); // promise reject: rejected
});
Promise.all特別適用於處理依賴多個異步請求的結果的場景.
Promise.race
語法: Promise.race(iterable)
該方法接一個迭代器(如數組等), 返回一個新的Promise對象. 只要迭代器中有一個Promise對象狀態改變(被實現或被拒絕), 那麼返回的Promise將以相同的值被實現或拒絕, 然後它將忽略迭代器中其他Promise的狀態變化.
Promise.race([1, Promise.reject(2)]).then(function(res){
console.log('promise fulfilled:', res);
}).catch(function(reason){
console.log('promise reject:', reason);
});
// promise fulfilled: 1
如果調換以上參數的順序, 結果將輸出 “promise reject: 2”. 可見對於狀態穩定的Promise(fulfilled 或 rejected狀態), 哪個排第一, 將返回哪個.
Promise.race適用於多者中取其一的場景, 比如同時發送多個請求, 只要有一個請求成功, 那麼就以該Promise的狀態作爲最終的狀態, 該Promise的值作爲最終的值, 包裝成一個新的Promise對象予以返回.
在 Fetch進階指南一文中, 我曾利用Promise.race模擬了Promise的abort和timeout機制.
Promises/A+規範的要點
promise.then(onFulfilled, onRejected)
中, 參數都是可選的, 如果onFulfilled或onRejected不是函數, 那麼將忽略它們.
catch只是then的語法糖, 相當於promise.then(null, onRejected)
.
任務隊列之謎
終於, 我們要一起來看看文章起始的一道題目.
setTimeout(function() {
console.log(4)
}, 0);
new Promise(function(resolve) {
console.log(1);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve()
}
console.log(2);
}).then(function() {
console.log(5)
});
console.log(3);
這道題目來自知乎(機智的你可能早已看穿, 但千萬別戳破), 可以戳此鏈接 Promise的隊列與setTimeout的隊列有何關聯 圍觀點贊.
圍觀完了, 別忘了繼續讀下去, 這裏請允許我站在諸位知乎大神的肩膀上, 繼續深入分析.
以上代碼, 最終運行結果是1,2,3,5,4. 並不是1,2,3,4,5.
- 首先前面有提到,
new Promise
第一個回調函數內的語句同步執行, 因此控制檯將順序輸出1,2, 此處應無異議. console.log(3)
, 這裏是同步執行, 因此接着將輸出3, 此處應無異議.- 剩下便是setTimeout 和 Promise的then的博弈了, 同爲異步事件, 爲什麼then後註冊卻先於setTimeout執行?
之前, 我們在 Ajax知識體系 一文中有提到:
瀏覽器中, js引擎線程會循環從 任務隊列 中讀取事件並且執行, 這種運行機制稱作 Event Loop (事件循環).
不僅如此, event loop至少擁有如下兩種隊列:
- task queue, 也叫macrotask queue, 指的是宏任務隊列, 包括rendering, script(頁面腳本), 鼠標, 鍵盤, 網絡請求等事件觸發, setTimeout, setInterval, setImmediate(node)等等.
- microtask queue, 指的是微任務隊列, 用於在瀏覽器重新渲染前執行, 包含Promise,
process.nextTick(node)
,Object.observe
, MutationObserver回調等.
如下是HTML規範原文:
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as: events, parsing, callbacks, using a resource, reacting to DOM manipulation…
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.
瀏覽器(或宿主環境) 遵循隊列先進先出原則, 依次遍歷macrotask queue中的每一個task, 不過每執行一個macrotask, 並不是立即就執行下一個, 而是執行一遍microtask queue中的任務, 然後切換GUI線程重新渲染或垃圾回收等.
上述代碼塊可以看做是一個macrotask, 對於其執行過程, 不妨作如下簡化:
- 首先執行當前macrotask, 將setTimeout回調以一個新的task形式, 加入到macrotask queue末尾.
- 當前macrotask繼續執行, 創建一個新的Promise, 同步執行其回調函數, 輸出1; for循環1w次, 然後執行resolve方法, 將該Promise回調加入到microtask queue末尾, 循環結束, 接着輸出2.
- 當前macrotask繼續執行, 輸出3. 至此, 當前macrotask執行完畢.
- 開始順序執行microtask queue中的所有任務, 也包括剛剛加入到隊列末尾 Promise回調, 故輸出5. 至此, microtask queue任務全部執行完畢, microtask queue清空.
- 瀏覽器掛起js引擎, 可能切換至GUI線程或者執行垃圾回收等.
- 切換回js引擎, 繼續從macrotask queue取出下一個macrotask, 執行之, 然後再取出microtask queue, 執行之, 後續所有的macrotask均如此重複. 自然, 也包括剛剛加入到隊列末尾的setTimeout回調, 故輸出4.
這裏直接給出事件回調優先級:
process.nextTick > promise.then > setTimeout ? setImmediate
nodejs中每一次event loop稱作tick. _tickCallback在macrotask queue中每個task執行完成後觸發. 實際上, _tickCallback內部共幹了兩件事:
- 執行nextTick queue中的所有任務, 包括
process.nextTick
註冊的回調. - 第一步完成後執行 _runMicrotasks函數, 即執行microtask queue中的所有任務, 包括
promise.then
註冊的回調.
因此,process.nextTick
優先級比promise.then
高.
那麼setTimeout與setImmediate到底哪個更快呢? 回答是並不確定. 請看如下代碼:
setImmediate(function(){
console.log(1);
});
setTimeout(function(){
console.log(0);
}, 0);
前後兩次的執行結果如下:
測試時, 我本地node版本是v5.7.0.
本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.
本文作者: louis
本文鏈接: http://louiszhai.github.io/2017/02/25/promise/
參考文章
完全理解Promise_JavaScript_第七城市
Promise - JavaScript | MDN
Promise的隊列與setTimeout的隊列的有何關聯 -知乎
HTML Standard event loop
Promises/A+
setImmediate API demo
Process.nextTick 和 setImmediate 的區別? - 知乎