一位不肯意透露姓名的頂級摸魚工程師曾經說過,學習 Promise 最好的方式就是先閱讀它的規範定義。那麼哪裏能夠找到 Promise 的標準定義呢?前端
答案是 Promises/A+ 規範。git
假設你已經打開了上述的規範定義的頁面並嘗試開始閱讀(不要由於是英文的就偷偷關掉,相信本身,你能夠的),規範在開篇描述了 Promise 的定義,與之交互的方法,而後強調了規範的穩定性。關於穩定性,換言之就是:咱們可能會修訂這份規範,可是保證改動微小且向下兼容,因此放心地學吧,這就是權威標準,五十年以後你再去谷歌 Promise,出來的規範仍是這篇 😂。github
好的,讓咱們回到規範。從開篇的介紹看,到底什麼是 Promise ?面試
A promise represents the eventual result of an asynchronous operation.ajax
Promise 表示一個異步操做的最終結果。編程
劃重點!!這裏其實引出了 JavaScript 引入 Promise 的動機:異步。promise
學習一門新技術,最好的方式是先了解它是如何誕生的,以及它所解決的問題是什麼。Promise 跟咱們說的異步編程有什麼聯繫呢?Promise 到底解決了什麼問題?瀏覽器
要回答這些問題,咱們須要先回顧下沒有 Promise 以前,異步編程存在什麼問題?markdown
JavaScript 的異步編程跟瀏覽器的事件循環息息相關,網上有不少的文章或專欄介紹了瀏覽器的事件循環機制,若是你還不瞭解,能夠先閱讀下面的文章,網絡
假設你已經瞭解了事件循環,接下來咱們來看異步編程存在什麼問題?
因爲 Web 頁面的單線程架構,決定了 JavaScript 的異步編程模型是基於消息隊列(Message Queue)和事件循環(Event Loop)的,就像下面這樣,
咱們的異步任務的回調函數會被放入消息隊列,而後等待主線程上的同步任務執行完成,執行棧爲空時,由事件循環機制調度進執行棧繼續執行。
這致使了 JavaScript 異步編程的一大特色:異步回調,好比網絡請求,
// 成功的異步回調函數
function resolve(response) {
console.log(response);
}
// 失敗的異步回調函數
function reject(error) {
console.log(error);
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => resolve(xhr.response);
xhr.ontimeout = (e) => reject(e);
xhr.onerror = (e) => reject(e);
xhr.open("Get", "http://xxx");
xhr.send();
複製代碼
雖然能夠經過簡單的封裝使得異步回調的方式變得優雅,好比,
$.ajax({
url: "https://xxx",
method: "GET",
fail: () => {},
success: () => {},
});
複製代碼
可是仍然沒有辦法解決業務複雜後的「回調地獄」的問題,好比多個依賴請求,
$.ajax({
success: function (res1) {
$.ajax({
success: function (res2) {
$.ajax({
success: function (res3) {
// do something...
},
});
},
});
},
});
複製代碼
這種線性的嵌套回調使得異步代碼變得難以理解和維護,也給人很大的心智負擔。 因此咱們須要一種技術,來解決異步編程風格的問題,這就是 Promise 的動機。
瞭解 Promise 背景和動機有利於咱們理解規範,如今讓咱們從新回到規範的定義。
Promise A+ 規範首先定義了 Promise 的一些相關術語和狀態。
then
方法的對象或函數,其行爲符合本規範then
方法的對象或函數undefined
, thenable
和 promise
)throw
語句拋出的一個值promise
的拒絕緣由promise 的當前狀態必須爲如下三種狀態之一:Pending
, Fulfilled
, Rejected
因此 Promise 內部其實維護了一個相似下圖所示的狀態機,
Promise 在建立時處於 Pending(等待態),以後能夠變爲 Fulfilled(執行態)或者 Rejected(拒絕態),一個承諾要麼被兌現,要麼被拒絕,這一過程是不可逆的。
定義了相關的術語和狀態後,是對 then
方法執行過程的詳細描述。
一個 promise 必須提供一個 then
方法以訪問其當前值、終值和拒絕緣由。
then
方法接受兩個參數,
promise.then(onFulfilled, onRejected);
複製代碼
對於這兩個回調參數和 then
的調用及返回值,有以下的一些規則,
onFulfilled 和 onRejected 都是可選參數。
onFulfilled 和 onRejected 必須做爲函數被調用,調用的 this
應用默認綁定規則,也就是在嚴格環境下,this
等於 undefined
,非嚴格模式下是全局對象(瀏覽器中就是 window
)。關於 this
的綁定規則若是不瞭解的能夠參考我以前的一篇文章 《多是最好的 this 解析了...》,裏面有很是詳細地介紹。
onFulfilled 和 onRejected 只有在執行環境堆棧僅包含平臺代碼時纔可被調用。因爲 promise 的實施代碼自己就是平臺代碼(JavaScript),這個規則能夠這麼理解:就是要確保這兩個回調在 then 方法被調用的那一輪事件循環以後異步執行。這不就是微任務的執行順序嗎?因此 promise 的實現原理是基於微任務隊列的。
then
方法能夠被同一個 promise 調用屢次,並且全部的成功或拒絕的回調需按照其註冊順序依次回調。因此 promise 的實現須要支持鏈式調用,能夠先想一下怎麼支持鏈式調用,稍後咱們會有對應的實現。
then
方法必須返回一個 promise 對象。
針對第 5 點,還有以下幾條擴展定義,咱們將返回值與 promise 的解決過程結合起來,
promise2 = promise1.then(onFulfilled, onRejected);
複製代碼
then
的兩個回調參數可能會拋出異常或返回一個值,
5.1 若是 onFulfilled 或者 onRejected 拋出一個異常 e
,那麼返回的 promise2 必須拒絕執行,並返回拒絕的緣由 e
。
5.2 若是 onFulfilled 或者 onRejected 返回了一個值 x
,會執行 promise 的解決過程
x
和返回的 promise2 相等,也就是 promise2 和 x
指向同一對象時,以 TypeError
做爲拒絕的緣由拒絕執行 promise2x
是 promise,會判斷 x
的狀態。若是是等待態,保持;若是是執行態,用相同的值執行 promise2;若是是拒絕態,用相同的拒絕緣由拒絕 promise2x
是對象或者函數,將 x.then
賦值給 then
;若是取 x.then
的值時拋出錯誤 e
,則以 e
爲拒絕緣由拒絕 promise2。若是 then
是函數,將 x
做爲函數的 this
,並傳遞兩個回調函數 resolvePromise, rejectPromise 做爲參數調用函數讀到這裏,相信你跟我同樣已經火燒眉毛想要實現一個 Promise 了,既然瞭解了原理和定義,咱們就來手寫一個 Promise 吧。
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
function resolve(value) {
return value;
}
function reject(err) {
throw err;
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>")
);
}
let called;
if ((typeof x === "object" && x != null) || typeof x === "function") {
try {
let then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.resolveCallbacks = [];
this.rejectCallbacks = [];
let resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.resolveCallbacks.forEach((fn) => fn());
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.rejectCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : resolve;
onRejected = typeof onRejected === "function" ? onRejected : reject;
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === PENDING) {
this.resolveCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.rejectCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
複製代碼
咱們從 Promise A+ 規範做爲切入點,先探索了 Promise 誕生的背景和動機,瞭解了異步編程的發展歷史,而後回到規範精讀了其中對於相關術語,狀態及執行過程的定義,最後嘗試了簡版的 Promise 實現。最新的 《JavaScript高級程序設計(第4版)》 中,將 Promise 翻譯爲 「承諾」,做爲現代 JavaScript 異步編程的方案,Promise 經過回調函數延遲綁定、回調函數返回值穿透和錯誤「冒泡」等技術解決了多層嵌套的問題,規範了對異步任務的處理結果(成功或失敗)的統一處理。
本文首發於個人 博客,才疏學淺,不免有錯誤,文章有誤之處還望不吝指正!
若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤
若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵
(完)