Promise使用手冊

本篇以Promise爲核心, 逐步展開, 最終分析process.nextTick , promise.then , setTimeout , setImmediate 它們的異步機制.javascript

導讀

Promise問世已久, 其科普類文章亦不可勝數. 遂本篇初衷不爲科普, 只爲可以溫故而知新.php

好比說, catch能捕獲全部的錯誤嗎? 爲何有些時候會拋出"Uncaught (in promise) …"? Promise.resolvePromise.reject 處理Promise對象時又有什麼不同的地方?html

Promise

引子

閱讀此篇以前, 咱們先體驗一下以下代碼:java

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);複製代碼

這裏先賣個關子, 後續將給出答案並提供詳細分析.node

和往常文章同樣, 我喜歡從api入手, 先具象地瞭解一個概念, 而後再抽象或擴展這個概念, 接着再談談概念的具體應用場景, 一般末尾還會有一個簡短的小結. 這樣, 查詢api的讀者能夠選擇性地閱讀上文, 但願深刻的讀者能夠繼續剖析概念, 固然我更但願你能耐心地讀到應用場景處, 這樣便能昇華對這個概念或技術的運用, 也能避免踩坑.react

new Promise

Promise的設計初衷是避免異步回調地獄. 它提供更簡潔的api, 同時展平回調爲鏈式調用, 使得代碼更加清爽, 易讀.git

以下, 即建立一個Promise對象:github

const p = new Promise(function(resolve, reject) {
  console.log('Create a new Promise.');
});
console.log(p);複製代碼

new Promise

建立Promise時, 瀏覽器同步執行傳入的第一個方法, 從而輸出log. 新建立的promise實例對象, 初始狀態爲等待(pending), 除此以外, Promise還有另外兩個狀態:web

  • fulfilled, 表示操做完成, 實現了. 只在resolve方法執行時才進入該狀態.
  • rejected, 表示操做失敗, 拒絕了. 只在reject方法執行時或拋出錯誤的狀況下才進入該狀態.

以下圖展現了Promise的狀態變化過程(圖片來自MDN):ajax

Promise state

從初始狀態(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對象.

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拋出的錯誤, 還會被進程的unhandledRejectionrejectionHandled事件捕獲.

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對象. 只要迭代器中有一個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.

  1. 首先前面有提到, new Promise第一個回調函數內的語句同步執行, 所以控制檯將順序輸出1,2, 此處應無異議.
  2. console.log(3), 這裏是同步執行, 所以接着將輸出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, 對於其執行過程, 不妨做以下簡化:

  1. 首先執行當前macrotask, 將setTimeout回調以一個新的task形式, 加入到macrotask queue末尾.
  2. 當前macrotask繼續執行, 建立一個新的Promise, 同步執行其回調函數, 輸出1; for循環1w次, 而後執行resolve方法, 將該Promise回調加入到microtask queue末尾, 循環結束, 接着輸出2.
  3. 當前macrotask繼續執行, 輸出3. 至此, 當前macrotask執行完畢.
  4. 開始順序執行microtask queue中的全部任務, 也包括剛剛加入到隊列末尾 Promise回調, 故輸出5. 至此, microtask queue任務所有執行完畢, microtask queue清空.
  5. 瀏覽器掛起js引擎, 可能切換至GUI線程或者執行垃圾回收等.
  6. 切換回js引擎, 繼續從macrotask queue取出下一個macrotask, 執行之, 而後再取出microtask queue, 執行之, 後續全部的macrotask均如此重複. 天然, 也包括剛剛加入到隊列末尾的setTimeout回調, 故輸出4.

這裏直接給出事件回調優先級:

process.nextTick > promise.then > setTimeout ? setImmediate複製代碼

nodejs中每一次event loop稱做tick. _tickCallback在macrotask queue中每一個task執行完成後觸發. 實際上, _tickCallback內部共幹了兩件事:

  1. 執行nextTick queue中的全部任務, 包括process.nextTick註冊的回調.
  2. 第一步完成後執行 _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

本文連接: louiszhai.github.io/2017/02/25/…

參考文章

相關文章
相關標籤/搜索