精讀《Promises/A+》規範

一位不肯意透露姓名的頂級摸魚工程師曾經說過,學習 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)的,就像下面這樣,

image.png

咱們的異步任務的回調函數會被放入消息隊列,而後等待主線程上的同步任務執行完成,執行棧爲空時,由事件循環機制調度進執行棧繼續執行。

這致使了 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 的一些相關術語和狀態。

Terminology,術語

  1. 「promise」 ,一個擁有 then 方法的對象或函數,其行爲符合本規範
  2. 「thenable」,一個定義了 then 方法的對象或函數
  3. 「value」,任何 JavaScript 合法值(包括 undefinedthenablepromise
  4. 「exception」,使用 throw 語句拋出的一個值
  5. 「reason」,表示一個 promise 的拒絕緣由

State,狀態

promise 的當前狀態必須爲如下三種狀態之一:PendingFulfilledRejected

  • 處於 Pending 時,promise 能夠遷移至 Fullfilled 或 Rejected
  • 處於 Fulfilled 時,promise 必須擁有一個不可變的終值且不能遷移至其餘狀態
  • 處於 Rejected 時,promise 必須擁有一個不可變的拒絕緣由且不能遷移至其餘狀態

因此 Promise 內部其實維護了一個相似下圖所示的狀態機,

image.png

Promise 在建立時處於 Pending(等待態),以後能夠變爲 Fulfilled(執行態)或者 Rejected(拒絕態),一個承諾要麼被兌現,要麼被拒絕,這一過程是不可逆的。

定義了相關的術語和狀態後,是對 then 方法執行過程的詳細描述。

Then

一個 promise 必須提供一個 then 方法以訪問其當前值、終值和拒絕緣由。

then 方法接受兩個參數,

promise.then(onFulfilled, onRejected);
複製代碼
  • onFulfilled,在 promise 執行結束後調用,第一個參數爲 promise 的終值
  • onRejected,在 promise 被拒絕執行後調用,第一個參數爲 promise 的拒絕緣由

對於這兩個回調參數和 then 的調用及返回值,有以下的一些規則,

  1. onFulfilled 和 onRejected 都是可選參數。

  2. onFulfilled 和 onRejected 必須做爲函數被調用,調用的 this 應用默認綁定規則,也就是在嚴格環境下,this 等於 undefined,非嚴格模式下是全局對象(瀏覽器中就是 window)。關於 this 的綁定規則若是不瞭解的能夠參考我以前的一篇文章 《多是最好的 this 解析了...》,裏面有很是詳細地介紹。

  3. onFulfilled 和 onRejected 只有在執行環境堆棧僅包含平臺代碼時纔可被調用。因爲 promise 的實施代碼自己就是平臺代碼(JavaScript),這個規則能夠這麼理解:就是要確保這兩個回調在 then 方法被調用的那一輪事件循環以後異步執行。這不就是微任務的執行順序嗎?因此 promise 的實現原理是基於微任務隊列的。

  4. then 方法能夠被同一個 promise 調用屢次,並且全部的成功或拒絕的回調需按照其註冊順序依次回調。因此 promise 的實現須要支持鏈式調用,能夠先想一下怎麼支持鏈式調用,稍後咱們會有對應的實現。

  5. 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 做爲拒絕的緣由拒絕執行 promise2
  • 若是 x 是 promise,會判斷 x 的狀態。若是是等待態,保持;若是是執行態,用相同的值執行 promise2;若是是拒絕態,用相同的拒絕緣由拒絕 promise2
  • 若是 x 是對象或者函數,將 x.then 賦值給 then;若是取 x.then 的值時拋出錯誤 e ,則以 e 爲拒絕緣由拒絕 promise2。若是 then 是函數,將 x 做爲函數的 this,並傳遞兩個回調函數 resolvePromise, rejectPromise 做爲參數調用函數

讀到這裏,相信你跟我同樣已經火燒眉毛想要實現一個 Promise 了,既然瞭解了原理和定義,咱們就來手寫一個 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,對做者也是一種鼓勵

(完)

相關文章
相關標籤/搜索