理解 Promise 的工做原理

Javascript 採用回調函數(callback)來處理異步編程。從同步編程到異步回調編程有一個適應的過程,可是若是出現多層回調嵌套,也就是咱們常說的厄運的回調金字塔(Pyramid of Doom),絕對是一種糟糕的編程體驗。因而便有了 CommonJS 的 Promises/A 規範,用於解決回調金字塔問題。本文先介紹 Promises 相關規範,而後再經過解讀一個迷你的 Promises 以加深理解。javascript

什麼是 Promise

一個 Promise 對象表明一個目前還不可用,可是在將來的某個時間點能夠被解析的值。它容許你以一種同步的方式編寫異步代碼。例如,若是你想要使用 Promise API 異步調用一個遠程的服務器,你須要建立一個表明數據將會在將來由 Web 服務返回的 Promise 對象。惟一的問題是目前數據還不可用。當請求完成並從服務器返回時數據將變爲可用數據。在此期間, Promise 對象將扮演一個真實數據的代理角色。接下來,你能夠在 Promise 對象上綁定一個回調函數,一旦真實數據變得可用這個回調函數將會被調用。java

Promise 對象曾經以多種形式存在於許多語言中。git

去除厄運的回調金字塔(Pyramid of Doom)

Javascript 中最多見的反模式作法是回調內部再嵌套回調。github

// 回調金字塔
asyncOperation(function(data){
  // 處理 `data`
  anotherAsync(function(data2){
      // 處理 `data2`
      yetAnotherAsync(function(){
          // 完成
      });
  });
});

引入 Promises 以後的代碼算法

promiseSomething()
.then(function(data){
    // 處理 `data`
    return anotherAsync();
})
.then(function(data2){
    // 處理 `data2`
    return yetAnotherAsync();
})
.then(function(){
    // 完成
});

Promises 將嵌套的 callback ,改形成一系列的.then的連綴調用,去除了層層縮進的糟糕代碼風格。 Promises 不是一種解決具體問題的算法,而已一種更好的代碼組織模式。接受新的組織模式同時,也逐漸以全新的視角來理解異步調用。編程

各個語言平臺都有相應的 Promise 實現promise

  • Java’s java.util.concurrent.Future
  • Python’s Twisted deferreds and PEP-3148 futures
  • F#'s Async<T>
  • .Net’s Task<T>
  • C++ 11’s std::future
  • Dart’s Future<T>
  • Javascript’s Promises/A/B/D/A+

下面我來相信瞭解一下 javascript 語言環境下各個規範的一些細節。服務器

Promises/A 規範

promise 表示一個最終值,該值由一個操做完成時返回。異步

  • promise 有三種狀態:未完成 (unfulfilled),完成 (fulfilled) 和失敗 (failed)。
  • promise 的狀態只能由未完成轉換成完成,或者未完成轉換成失敗 。
  • promise 的狀態轉換隻發生一次。

promise 有一個 then 方法, then 方法能夠接受 3 個函數做爲參數。前兩個函數對應 promise 的兩種狀態 fulfilled 和 rejected 的回調函數。第三個函數用於處理進度信息(對進度回調的支持是可選的)。async

promiseSomething().then(function(fulfilled){
        //當 promise 狀態變成 fulfilled 時,調用此函數
    },function(rejected){
        //當 promise 狀態變成 rejected 時,調用此函數
    },function(progress){
        //當返回進度信息時,調用此函數
    });

若是 promise 支持以下連個附加方法,稱之爲可交互的 promise

  • get(propertyName) 得到當前 promise 最終值上的一個屬性,返回值是一個新的 promise 。
  • call(functionName, arg1, arg2, …) 調用固然 promise 最終值上的一個方法,返回值也是一個新的 promise 。

Promises/B 規範

在 Promises/A 的基礎上, Promises/B 定義了一組 promise 模塊須要實現的 API

when(value, callback, errback_opt) 若是 value 不是一個 promise ,那麼下一事件循環 callback 會被調用, value 做爲 callback 的傳入值。若是 value 是一個 promise , promise 的狀態已經完成或者變成完成時,那麼下一事件循環 callback 會被調用, resolve 的值會被傳入 callback ; promise 的狀態已經失敗或者變成失敗時,那麼下一事件循環 errback 會被調用, reason 會做爲失敗的理由傳入 errback 。

asap(value, callback, errback_opt) 與 when 最大的區別,若是 value 不是一個 promise ,會被當即執行,不會等到下一事件循環。

enqueue(task Function) 儘量快地在接下來的事件循環調用 task 方法。

get(object, name) 返回一個得到對象屬性的 promise 。

post(object, name, args) 返回一個調用對象方法的 promise 。

put(object, name, value) 返回一個修改對象屬性的 promise 。

del(object, name) 返回一個刪除對象屬性的 promise 。

makePromise(descriptor Object, fallback Function) 返回一個 promise 對象,該對象必須是一個可調用的函數,也多是可被實例化的構造函數。

  • 第一個參數接受一個描述對象,該對象結構以下,
{
        "when": function(errback){...},
        "get": function(name){...},
        "put": function(name, value){...},
        "post": function(name, args){...},
        "del": function(name){...},
    }

上面每個註冊的 handle 都返回一個 resolved value 或者 promise 。

  • 第二個參數接受一個 fallback(message,…args) 函數,當沒有 promise 對象沒有找到對應的 handle 時該函數會被觸發,返回一個 resolved value 或者 promise 。

defer() 返回一個對象,該對象包含一個 resolve(value) 方法和一個 promise 屬性。 當 resolve(value) 方法被第一次調用時, promise 屬性的狀態變成 完成,全部以前或以後觀察該 promise 的 promise 的狀態都被轉變成 完成。 value 參數若是不是一個 promise ,會被包裝成一個 promise 的 ref 。 resolve 方法會忽略以後的全部調用。

reject(reason String) 返回一個被標記爲 失敗 的 promise 。 一個失敗的 promise 上被調用 when(message) 方法時,會採用以下兩種方法之一

  1. 若是存在 errback , errback 會以 reason 做爲參數被調用。 when 方法會將 errback 的返回值返回。
  2. 若是不存在 errback , when 方法返回一個新的 reject 狀態的 promise 對象,以同一 reason 做爲參數。

ref(value) 若是 value 是 promise 對象,返回 value 自己。不然,返回一個 resolved 的 promise ,攜帶以下 handle 。

  1. when(errback),忽略 errback ,返回 resolved 值
  2. get(name),返回 resolved 值的對應屬性。
  3. put(name, value) ,設置 resolved 值的對應屬性。
  4. del(name),刪除 resolved 值的對應屬性。
  5. post(name, args), 調用 resolved 值的對應方法。
  6. 其餘全部的調用都返回一個 reject ,並攜帶 「Promise does not handle NAME」 的理由。

isPromise(value) Boolean 判斷一個對象是不是 promise

method(name String) 得到一個返回 name 對應方法的 promise 。返回值是 「get」, 「put」, 「del」 和 「post」 對應的方法,可是會在下一事件循環返回。

Promises/D 規範

爲了增長不一樣 promise 實現之間的可互操做性, Promises/D 規範對 promise 對象和 Promises/B 規範作了進一步的約定。以達到鴨子類型的效果( Duck-type Promise )。

簡單來講 Promises/D 規範,作了兩件事情,

  1. 如何判斷一個對象是 Promise 類型。
  2. 對 Promises/B 規範進行細節補充。

甄別一個 Promise 對象

Promise 對象必須是實現 promiseSend 方法。

  1. 在 promise 庫上下文中,若是對象包含 promiseSend 方法就能夠甄別爲 promise 對象
  2. promiseSend 方法必須接受一個操做名稱,做爲第一個參數
  3. 操做名稱是一個可擴展的集合,下面是一些保留名稱
    1. when,此時第三個參數必須是 rejection 回調。
      1. rejection 回調必須接受一個 rejection 緣由(能夠是任何值)做爲第一個參數
    2. get,此時第三個參數爲屬性名(字符串類型)
    3. put,此時第三個參數爲屬性名(字符串類型),第四個參數爲新屬性值。
    4. del,此時第三個參數爲屬性名
    5. post,此時第三個參數爲方法的屬性名,接下來的變參爲方法的調用參數
    6. isDef
  4. promiseSend方法的第二個參數爲 resolver 方法
  5. promiseSend方法可能接受變參
  6. promiseSend方法必須返回undefined

對 Promises/B 規範的補充

Promises/D 規範中對 Promises/B 規範中定義的 ref 、 reject 、 def 、 defer 方法作了進一步細緻的約束,此處略去這些細節。

Promises/A+ 規範

前面提到的 Promises/A/B/D 規範都是有 CommonJS 組織提出的, Promises/A+是有一個自稱爲Promises/A+ 組織發佈的,該規範是以 Promises/A 做爲基礎進行補充和修訂,旨在提升 promise 實現之間的可互操做性。

Promises/A+ 對.then方法進行細緻的補充,定義了細緻的Promise Resolution Procedure流程,而且將.then方法做爲 promise 的對象甄別方法。

此外, Promises/A+ 還提供了兼容性測試工具,以肯定各個實現的兼容性。

實現一個迷你版本的 Promise

上面扯了這麼多規範,如今咱們看看如何實現一個簡單而短小的 Promise 。

狀態機

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

function Promise() {
  // store state which can be PENDING, FULFILLED or REJECTED
  var state = PENDING;

  // store value or error once FULFILLED or REJECTED
  var value = null;

  // store sucess & failure handlers attached by calling .then or .done
  var handlers = [];
}

狀態變遷

僅支持兩種狀態變遷, fulfill 和 reject

// ...

function Promise() {
    // ...

  function fulfill(result) {
    state = FULFILLED;
    value = result;
  }

  function reject(error) {
    state = REJECTED;
    value = error;
  }

}

fulfill 和 reject 方法較爲底層,一般更高級的 resolve 方法開放給外部。

// ...

function Promise() {

  // ...

  function resolve(result) {
    try {
      var then = getThen(result);
      if (then) {
        doResolve(then.bind(result), resolve, reject)
        return
      }
      fulfill(result);
    } catch (e) {
      reject(e);
    }
  }
}

resolve 方法能夠接受一個普通值或者另外一個 promise 做爲參數,若是接受一個 promise 做爲參數,等待其完成。 promise 不容許被另外一個 promise fulfill ,因此須要開放 resolve 方法。 resolve 方法依賴一些幫助方法定義以下:

/**
 * Check if a value is a Promise and, if it is,
 * return the `then` method of that promise.
 *
 * @param {Promise|Any} value
 * @return {Function|Null}
 */
function getThen(value) {
  var t = typeof value;
  if (value && (t === 'object' || t === 'function')) {
    var then = value.then;
    if (typeof then === 'function') {
      return then;
    }
  }
  return null;
}

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 *
 * @param {Function} fn A resolver function that may not be trusted
 * @param {Function} onFulfilled
 * @param {Function} onRejected
 */
function doResolve(fn, onFulfilled, onRejected) {
  var done = false;
  try {
    fn(function (value) {
      if (done) return
      done = true
      onFulfilled(value)
    }, function (reason) {
      if (done) return
      done = true
      onRejected(reason)
    })
  } catch (ex) {
    if (done) return
    done = true
    onRejected(ex)
  }
}

這裏 resolve 和 doResolve 之間的遞歸很巧妙,用來處理 promise 的層層嵌套( promise 的 value 是一個 promise )。

構造器

// ...

function Promise(fn) {
    // ...
    doResolve(fn, resolve, reject);
}

.done 方法

// ...
function Promise(fn) {
  // ...

  function handle(handler) {
    if (state === PENDING) {
      handlers.push(handler);
    } else {
      if (state === FULFILLED &&
        typeof handler.onFulfilled === 'function') {
        handler.onFulfilled(value);
      }
      if (state === REJECTED &&
        typeof handler.onRejected === 'function') {
        handler.onRejected(value);
      }
    }
  }

  this.done = function (onFulfilled, onRejected) {
    // ensure we are always asynchronous
    setTimeout(function () {
      handle({
        onFulfilled: onFulfilled,
        onRejected: onRejected
      });
    }, 0);
  }
  // ...
}

.then 方法

// ...
function Promise(fn) {
    // ...
    this.then = function (onFulfilled, onRejected) {
      var self = this;
      return new Promise(function (resolve, reject) {
        return self.done(function (result) {
          if (typeof onFulfilled === 'function') {
            try {
              return resolve(onFulfilled(result));
            } catch (ex) {
              return reject(ex);
            }
          } else {
            return resolve(result);
          }
        }, function (error) {
          if (typeof onRejected === 'function') {
            try {
              return resolve(onRejected(error));
            } catch (ex) {
              return reject(ex);
            }
          } else {
            return reject(error);
          }
        });
      });
    }
    // ...
}

$.promise

jQuery 1.8 以前的版本, jQuery 的 then 方法只是一種能夠同時調用 done 、 fail 和 progress 這三種回調的速寫方法,而 Promises/A 規範的 then 在行爲上更像是 jQuery 的 pipe 。 jQuery 1.8 修正了這個問題,使 then 成爲 pipe 的同義詞。不過,因爲向後兼容的問題, jQuery 的 Promise 再如何對 Promises/A 示好也不太會招人待見。

此外,在 Promises/A 規範中,由 then 方法生成的 Promise 對象是已執行仍是已拒絕,取決於由 then 方法調用的那個回調是返回值仍是拋出錯誤。在 JQuery 的 Promise 對象的回調中拋出錯誤是個糟糕的主意,由於錯誤不會被捕獲。

小結

最後一個例子揭示了,實現 Promise 的關鍵是實現好 doResolve 方法,在完事之後觸發回調。而爲了保證異步 setTimeout(fun, 0); 是關鍵一步。

Promise 一直用得蠻順手的,其很好的優化了 NodeJS 異步處理時的代碼結構。可是對於其工做原理卻有些懵懂和好奇。因而花了些經理查閱並翻譯了 Promise 的規範,以充分的理解 Promise 的細節。

相關文章
相關標籤/搜索