本篇以Promise爲核心, 逐步展開, 最終分析process.nextTick , promise.then , setTimeout , setImmediate 它們的異步機制.javascript
Promise問世已久, 其科普類文章亦不可勝數. 遂本篇初衷不爲科普, 只爲可以溫故而知新.php
好比說, catch能捕獲全部的錯誤嗎? 爲何有些時候會拋出"Uncaught (in promise) …"? Promise.resolve
和 Promise.reject
處理Promise對象時又有什麼不同的地方?html
閱讀此篇以前, 咱們先體驗一下以下代碼: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
Promise的設計初衷是避免異步回調地獄. 它提供更簡潔的api, 同時展平回調爲鏈式調用, 使得代碼更加清爽, 易讀.git
以下, 即建立一個Promise對象:github
const p = new Promise(function(resolve, reject) {
console.log('Create a new Promise.');
});
console.log(p);複製代碼
建立Promise時, 瀏覽器同步執行傳入的第一個方法, 從而輸出log. 新建立的promise實例對象, 初始狀態爲等待(pending), 除此以外, Promise還有另外兩個狀態:web
以下圖展現了Promise的狀態變化過程(圖片來自MDN):ajax
從初始狀態(pending)到實現(fulfilled)或拒絕(rejected)狀態的轉換, 這是兩個分支, 實現或拒絕即最終狀態, 一旦到達其中之一的狀態, promise的狀態便穩定了. (所以, 不要嘗試實現或拒絕狀態的互轉, 它們都是最終狀態, 無法轉換)
以上, 建立Promise對象時, 傳入的回調函數function(resolve, reject){}
默認擁有兩個參數, 分別爲:
Promise的原型僅有兩個自身方法, 分別爲 Promise.prototype.then
, Promise.prototype.catch
. 而它自身僅有四個方法, 分別爲 Promise.reject
, Promise.resolve
, Promise.all
, Promise.race
.
語法: 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.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能夠捕獲全部錯誤呢? 能夠, 怎麼不能夠, 我之前也這麼天真的認爲. 直到有一天我執行了以下的語句, 我就學乖了.
new Promise(function(resolve, reject){
Promise.reject('返回一個拒絕狀態的Promise');
}).catch(function(reason){
console.log('catch:', reason);
});複製代碼
執行結果以下:
爲何catch沒有捕獲到該錯誤呢? 這個問題, 待下一節咱們瞭解了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(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.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對象. 只要迭代器中有一個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機制.
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.
console.log(3)
, 這裏是同步執行, 所以接着將輸出3, 此處應無異議.以前, 咱們在 Ajax知識體系 一文中有提到:
瀏覽器中, js引擎線程會循環從
任務隊列
中讀取事件而且執行, 這種運行機制稱做Event Loop
(事件循環).
不只如此, event loop至少擁有以下兩種隊列:
以下是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, 對於其執行過程, 不妨做以下簡化:
這裏直接給出事件回調優先級:
process.nextTick > promise.then > setTimeout ? setImmediate複製代碼
nodejs中每一次event loop稱做tick. _tickCallback在macrotask queue中每一個task執行完成後觸發. 實際上, _tickCallback內部共幹了兩件事:
所以, 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/…
參考文章