原文:Javascript高級編程4html
同步行爲與異步行爲之間的對偶是計算機科學中的一個基本概念,尤爲是在單線程事件循環模型(如JavaScript)中。面對高延遲操做,異步行爲再也不須要針對更高的計算吞吐量進行優化。若是在計算完成時仍然能夠運行其餘指令而且仍保持穩定的系統,那麼這樣作是實用的。編程
更重要的是,異步操做不必定是計算密集型操做或高延遲操做。它能夠在不須要阻塞執行線程以等待異步行爲發生的任何地方使用。promise
同步行爲相似於內存中的順序處理器指令。每條指令嚴格按照其出現的順序執行,而且每條指令還可以當即檢索系統本地存儲的信息(例如:在處理器寄存器或系統內存中)。結果,很容易推斷出代碼中任何給定點的程序狀態(例如,變量的值)。瀏覽器
一個簡單的例子就是執行一個簡單的算術運算:安全
let x = 3;
x = x + 4;
複製代碼
在該程序的每一個步驟中,均可以推斷出程序的狀態,由於在完成前一條指令以前,執行不會繼續進行。當最後一條指令完成時,x的計算值當即可用。全部這些指令都在單個執行線程中串行存在。服務器
相反,異步行爲相似於中斷,即當前進程外部的實體可以觸發代碼執行。一般須要異步操做,由於強制操做等待較長時間才能完成操做是不可行的(同步操做就是這種狀況)。因爲代碼正在訪問高延遲資源,例如將請求發送到遠程服務器並等待響應,所以可能會發生長時間等待。閉包
一個簡單的JavaScript示例將在超時內執行算術運算:異步
let x = 3;
setTimeout(() => x = x + 4, 1000);
複製代碼
該程序最終執行與一個同步程序相同的工做(將兩個數字加在一塊兒),可是該執行線程沒法確切知道x的值什麼時候會更改,由於這取決於什麼時候從消息隊列中使回調出隊並執行回調。async
長期以來,異步操做一直是JavaScript語言的痛點。在該語言的早期版本中,異步操做僅支持定義回調函數以指示異步操做已完成。異步行爲的執行是一個常見的問題,一般能夠經過一個充滿嵌套回調函數的代碼片斷來解決,該代碼片斷一般稱爲「回調地獄」。異步編程
getData(function(a){
getMoreData(a, function(b){
getMoreData(b, function(c){
getMoreData(c, function(d){
getMoreData(d, function(e){
// todo
});
});
});
});
});
複製代碼
假設setTimeout
操做返回了一個有用的值。將值傳回的最佳方式是什麼?普遍接受的策略是提供對異步操做的回調,其中該回調包含須要訪問計算值(做爲參數提供)的代碼。以下所示:
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`給我: ${x}`));
// 給我: 6 (大約1000ms後打印)
複製代碼
此處,setTimeout
調用在通過1000毫秒後將函數推入消息隊列。此函數將由運行時出隊並異步求值。回調函數及其參數仍然能夠經過函數閉包在異步執行中使用。
Promise
表示某種還沒有產生結果的實體。例如最終(eventual)
,將來(future)
,延遲(delay)
或推遲(deferred)
。全部這些都以一種或另外一種形式描述了一種用於同步程序執行的編程工具。
一份針對健全、通用JavaScript promises對象的開放標準 — 由實現者制定,供實現者參考。
一個 promise 對象表明一個異步操做的最終結果。與promise進行交互的主要方式是經過它的 then 方法,經過該方法註冊回調函數,進而接受promise對象最終的值(value)或不能完成(fulfill)的緣由(reason)。
ECMAScript 6引入了Promises/A+
兼容Promise類型的一等實現。自推出以來,Promises的採用率就很是高。全部現代瀏覽器都徹底支持ES6 Promise類型,而且多個瀏覽器API(例如fetch()和Battery API)僅使用它。
當將promise實例傳遞到console.log時,控制檯輸出(可能因瀏覽器而異)指示此promise實例處於待定(pending)
狀態。如前所述,promise是一個有狀態對象,能夠存在如下三種狀態之一:
待定(pending)
狀態是promise開始的初始狀態。從待定(pending)
狀態開始,一個promise能夠轉換到fulfilled
狀態(表示成功)或rejected
狀態(表示失敗)。這種過渡到穩定(settled
)狀態是不可逆的。一旦變成已執行(fulfilled
)狀態或被拒絕(rejected
)狀態,promise的狀態就永遠不會改變。此外,不能保證promise未來會離開待定(pending)
狀態。所以,無論promise處於何種狀態,即成功執行
、拒絕
或從未退出待定(pending)狀態
,結構良好的代碼都應能正常運行。
更重要的是,promise的狀態是私有的,不能在JavaScript中直接檢查。這樣作的緣由主要是爲了防止在讀取promise對象時根據其狀態進行同步編程處理。此外,外部JavaScript沒法更改Promise的狀態。
因爲promise狀態是私有的,所以它只能在內部被維護操做。這種內部操做是在promise的執行者(executor
)函數內部執行的。執行函數有兩個主要職責:初始化promise的異步行爲,以及控制任何最終的狀態轉換。經過調用狀態轉換的兩個函數參數之一(一般命名爲resolve
和reject
)來完成對狀態轉換的控制。調用resolve
將使狀態變爲已實現fulfilled
;調用reject
會將狀態更改成拒絕rejected
。調用rejected()
也會引起錯誤。
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)
複製代碼
一旦調用resolve
或reject
,狀態轉移將沒法撤消。試圖進一步改變狀態的嘗試將會被忽略。以下所示:
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 被忽略
});
setTimeout(console.log, 0, p); // Promise <resolved>
複製代碼
您能夠經過添加定時退出行爲來避免Promise陷入待定(pending)
狀態。例如,您能夠設置超時以在10秒後拒絕這個promise:
let p = new Promise((resolve, reject) => {
setTimeout(reject, 10000); // 10秒後, 調用reject()
// 執行其餘代碼
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 11000, p); // 11秒後檢查狀態
// (10秒後) Uncaught error
// (11秒後) Promise <rejected>
複製代碼
由於一個promise只能更改狀態一次,因此此超時行爲使您能夠安全地設置一個能夠保持在待定(pending)
狀態的時間的最大值。若是執行程序內部的代碼要在超時以前resolve
或reject
,則超時處理程序拒絕reject
的嘗試將被忽略。
promise不必定須要從待定(pending)
狀態開始並利用執行程序函數來達到穩定settled
狀態。經過調用Promise .resolve()靜態方法,能夠在resolved
狀態下實例化Promise。如下兩個promise實例其實是等效的:
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
複製代碼
此已解決resolved
的Promise的值將成爲傳遞給Promise.resolve()的第一個參數。這有效地使您能夠將任何值轉成
一個promise:
setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3
// Additional arguments are ignored
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4
複製代碼
也許此靜態方法最重要的用處之一是當參數已是一個promise實例時,它能夠充當傳遞passthrough
的能力。也就是說,Promise.resolve()是一個冪等方法,如此處所示:
let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true
複製代碼
這種冪等操做將保持傳遞給它的promise的狀態:
let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
複製代碼
但請注意,此靜態方法將愉快地將任何非promise(包括錯誤對象)包裝爲已解決resolved
的promise,這可能會致使意外的行爲:
let p = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo
複製代碼
與Promise.resolve()的概念相似,Promise.reject()實例化一個被拒絕rejected
的promise並引起異步錯誤(try/catch不會捕獲該異步錯誤,而該錯誤只能由拒絕處理程序捕獲)。如下兩個promise實例其實是等效的:
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
複製代碼
此已解決的promise的緣由(reason)
字段將是傳遞給Promise.reject()的第一個參數。它也會被傳遞給拒絕處理程序:
let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
複製代碼
更重要的是,Promise.reject()不能反映等冪性的Promise.resolve()行爲。若是傳遞了一個promise對象,它將很樂意使用該promise做爲被拒絕promise的緣由(reason)
字段:
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>
複製代碼
Promise構造的許多設計都是爲了在JavaScript中產生一種徹底獨立的計算模式。在下面的示例中,它被巧妙地封裝,從而以兩種不一樣的方式引起錯誤:
try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch(e) {
console.log(e);
}
// Uncaught (in promise) Error: bar
複製代碼
第一個try/catch塊拋出一個錯誤,而後繼續捕獲它,可是第二個try/catch塊拋出了一個未被捕獲的錯誤。這彷佛是違反直覺的,由於代碼彷佛是在同步建立被拒絕的Promise實例,而後在被拒絕時引起錯誤。可是,未捕獲第二個promise的緣由是代碼沒有嘗試在適當的異步模式
下捕獲錯誤。這種行爲強調了promise的實際行爲:它們是同步對象-在同步執行模式內使用-充當通往異步執行模式的橋樑。
在前面的示例中,來自被拒絕的promise的錯誤不是在同步執行線程中引起的,而是在瀏覽器的異步消息隊列執行中引起的。所以,封裝try/catch塊不足以捕獲此錯誤。一旦代碼開始以這種異步模式執行,與之交互的惟一方法就是使用異步模式構造—更具體地說,是promise方法。
在promise實例上公開的方法用於彌合同步外部代碼路徑和異步內部代碼路徑之間的差距。這些方法可用於訪問從異步操做返回的數據,處理promise的成功和失敗結果,串行評估promise或添加僅在promise進入終端狀態後才執行的功能。
出於ECMAScript異步構造的目的,任何公開了then()方法的對象都被視爲實現了Thenable接口。如下是實現此接口的最簡單類的示例:
class MyThenable {
then() {}
}
複製代碼
ECMAScript Promise類型實現了Thenable接口。不要將這種簡單化的接口與諸如TypeScript之類的包中的其餘接口或類型定義相混淆,後者提供了thenable接口的更具體形式。
Promise.prototype.then()方法是用於將處理程序附加到Promise實例的主要方法。then()方法最多接受兩個參數:一個可選的onResolved處理函數和一個可選的onRejected處理函數。每一個僅在定義了它們的promise達到其各自的已實現(fulfilled)
或已拒絕(rejected)
狀態時才執行。
function onResolved(id) {
setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(() => onResolved('p1'),
() => onRejected('p1'));
p2.then(() => onResolved('p2'),
() => onRejected('p2'));
// (3秒後)
// p1 resolved
// p2 rejected
複製代碼
由於一個promise只能轉換一次到最終狀態,因此能夠保證這些處理函數的執行是互斥的。
如前所述,兩個處理程序參數都是徹底可選的。做爲then()的參數提供的任何非函數類型都將被靜默忽略。若是隻想顯式地提供onRejected處理程序,則將undefined做爲onResolved參數是一種典型選擇。這樣能夠避免在內存中建立臨時對象,以避免被解釋器忽略。
function onResolved(id) {
setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
// 非函數類型參數將被靜默忽略,不建議使用
p1.then('hello');
// 顯式跳過onResolved處理程序
p2.then(null, () => onRejected('p2'));
// p2 rejected (3秒後)
複製代碼
Promise.prototype.then()方法返回一個新的Promise實例:
let p1 = new Promise(() => {});
let p2 = p1.then();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
複製代碼
這個新的Promise實例p2
是從onResolved處理程序的返回值派生的。處理程序的返回值包裝在Promise.resolve()中以生成新的Promise。若是未提供處理函數,則該方法將直接傳遞初始promise的已解決的值。若是沒有顯式的return語句,則默認的返回值是undefined
,幷包裝在Promise.resolve()中。
let p1 = Promise.resolve('foo');
// 調用then()方法時,沒有提供處理函數參數,p1.then()的結果是直接返回p1給p2
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
// 這些是等效的
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
複製代碼
把顯式返回值包裝在Promise.resolve()中:
// 這些是等效的:
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar
// 1, 處理程序的返回值包裝在Promise.resolve()中以生成新的Promise
// 2, Promise.resolve()保留返回的promise
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined
複製代碼
拋出異常將返回被拒絕的promise:
let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected> baz
複製代碼
更重要的是,返回錯誤不會觸發相同的拒絕行爲,而是將錯誤對象包裝在已解決的Promise中:
let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux
複製代碼
onRejected處理函數的處理方式也相同:從onRejected處理函數返回的值包裝在Promise.resolve()中。乍一看,這彷佛違反直覺,可是onRejected處理程序正在執行其工做以捕獲異步錯誤。所以,該拒絕處理函數在不引起其餘錯誤的狀況下完成執行應視爲預期的promise行爲,並所以返回已解決的promise。
如下Promise.reject()代碼片斷和使用Promise.resolve()的先前示例相似:
let p1 = Promise.reject('foo');
// 調用then()方法時,沒有提供處理函數參數,p1.then()的結果是直接返回p1給p2
let p2 = p1.then();
// Uncaught (in promise) foo
setTimeout(console.log, 0, p2); // Promise <rejected>: foo
// 這些是等效的:
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
// 這些是等效的:
let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar
// Promise.resolve()保留返回的promise
let p8 = p1.then(null, () => new Promise(() => {}));
let p9 = p1.then(null, () => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined
let p10 = p1.then(null, () => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: baz
let p11 = p1.then(null, () => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux
複製代碼
Promise.prototype.catch()方法只能用於將拒絕處理函數附加到Promise。它只須要一個參數,即onRejected處理函數。該方法僅是語法糖,與使用Promise.prototype.then(null,onRejected)並沒有不一樣。
下面的代碼演示了這種等效性:
let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
// 這兩個拒絕處理程序的行爲相同:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
複製代碼
Promise.prototype.catch()方法返回一個新的Promise實例:
let p1 = new Promise(() => {});
let p2 = p1.catch();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
複製代碼
關於建立新的Promise實例,Promise.prototype.catch()的行爲與Promise.prototype.then()的onRejected處理程序相同。
Promise.protoype.finally()方法可用於附加onFinally處理程序,該處理程序在promise達到已解決或已拒絕狀態時執行。這對於避免onResolved和onRejected處理程序之間的代碼重複頗有用。重要的是,處理程序沒有任何方法能夠肯定promise是否已解決或被拒絕,所以該方法旨在用於清除之類的事情。
let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
setTimeout(console.log, 0, 'Finally!')
}
p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally
複製代碼
Promise.prototype.finally()方法返回一個新的Promise實例:
let p1 = new Promise(() => {});
let p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
複製代碼
這個新的Promise實例是經過不一樣於then()或catch()的方式派生的。由於onFinally旨在成爲狀態未知的方法,因此在大多數狀況下,它將做爲直接傳遞父promose的做用。不管是已解決狀態仍是被拒絕狀態,都是如此。
let p1 = Promise.resolve('foo');
// 這些都充當直接傳遞以前的promise, 即p1
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo
複製代碼
惟一的例外是它返回待定的promise或引起錯誤(經過顯式throw或返回被拒絕的promise)。在這些狀況下,將返回相應的promise(待定或拒絕),以下所示:
// Promise.resolve()保留返回的promise
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
let p11 = p1.finally(() => { throw 'baz';});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz
複製代碼
返回待定的promise是一種不常見的狀況,由於一旦promise解決,新的promise仍將充當初始promise來傳遞:
let p1 = Promise.resolve('foo');
// resolve('bar')將被忽略
let p2 = p1.finally(
() => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100)));
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(() => setTimeout(console.log, 0, p2), 200);
// 200毫秒後:
// Promise <resolved>: foo
複製代碼
長期以來,在單線程JavaScript運行時內部掌握異步行爲一直是一項艱鉅的任務。隨着ES6中引入Promise和ES7中引入async/await ,ECMAScript中的異步構造獲得了極大的加強。Promise和async/await不只啓用了之前難以實現或沒法實現的模式,並且還帶來了一種全新的JavaScript編寫方式,該方式更加簡潔,簡短,易於理解和調試。它們是現代JavaScript工具箱中最重要的工具之一。