寫一個符合 Promises/A+ 規範並可配合 ES7 async/await 使用的 Promise

原文地址javascript


從歷史的進程來看,Javascript 異步操做的基本單位已經從 callback 轉換到 Promise。除了在特殊場景使用 streamRxJs 等更加抽象和高級的異步數據獲取和操做流程方式外,如今幾乎任何成熟的異步操做庫,都會實現或者引用 Promise 做爲 API 的返回單位。主流的 Javascript 引擎也基本原生實現了 Promise。php

在 Promise 遠未流行之前,Javascript 的異步操做基本都在使用以 callback 爲主的異步接口。鼠標鍵盤事件,頁面渲染,網絡請求,文件請求等等異步操做的回調函數都是用 callback 來處理。隨着異步使用場景範圍的擴大,出現了大量工程化和富應用的的交互和操做,使得應用不足以用 callback 來面對越發複雜的需求,慢慢出現了許多優雅和先進的異步解決方案:EventEmitterPromiseWeb WorkerGeneratorAsync/Awaithtml

目前 Javascript 在客戶端和服務器端的應用當中,只有 Promise 被普遍接受並使用。追根溯源,Promise 概念的提出是在 1976 年,Javascript 最先的 Promise 實現是在 2007 年,到如今 2016 年,Promise/A+ 規範和 ECMAscript 規範提出的 API 也足夠穩定。then, reject, all, spread, race, finally 都是工程師開發中常常用到的 Promise API。不少人剛接觸 Promise 概念的時候看下 API,看幾篇博客或者看幾篇最佳實踐就覺得理解程度夠了,可是對 Promise 內部的異步機制不明瞭,使得在開發過程當中遇到很多坑或者懵逼。java

本文旨在讓讀者能深刻了解 Promise 內部執行機制,熟悉和掌握 Promise 的操做流。若有興趣,能夠繼續往下讀。react

Promise 只是一個 Event Loop 中的 microtask

深刻了解過 Promise 的人都知道,Promise 所說的異步執行,只是將 Promise 構造函數中 resolvereject 方法和註冊的 callback 轉化爲 eventLoop的 microtask/Promise Job,並放到 Event Loop 隊列中等待執行,也就是 Javascript 單線程中的「異步執行」。git

Promise/A+ 規範中,並無明確是以 microtask 仍是 macrotask 形式放入隊列,對沒有 microtask 概念的宿主環境採用 setTimeout 等 task/Job 類的任務。規範中另外明確的一點也很是重要:回調函數的異步調用必須在當前 context,也就是 JS stack 爲空以後執行。es6

在最新的 ECMAScript 規範 中,明確了 Promise 必須以 Promise Job 的形式入 Job 隊列(也就是 microtask),並僅在沒有運行的 stack(stack 爲空的狀況下)才能夠初始化執行。github

HTML 規範 也提出,在 stack 清空後,執行 microtask 的檢查方法。也就是必須在 stack 爲空的狀況下才能執行。web

Google Chrome 的開發者 Jake Archibald (ES6-promise 做者)的文章 Tasks, microtasks, queues and schedules中,將這個區分的問題描述得很清楚。假如要在 Javascript 平臺或者引擎中實現 Promise,優先以 microtask/Promise Job 方式實現。目前主流瀏覽器的 Javascript 引擎原生實現,主流的 Promise 庫(es6-promise,bluebrid)基本都是使用 microtask/Promise Job 的形式將 Promise 放入隊列。api

其餘以 microtask/Promise Job 形式實現的方法還有:process.nextTicksetImmediatepostMessageMessageChannel

根據規範,microtask 存在的意義是:在當前 task 執行完,準備進行 I/O,repaintredraw 等原生操做以前,須要執行一些低延遲的異步操做,使得瀏覽器渲染和原生運算變得更加流暢。這裏的低延遲異步操做就是 microtask。原生的 setTimeout 就算是將延遲設置爲 0 也會有 4 ms 的延遲,會將一個完整的 task 放進隊列延遲執行,並且每一個 task 之間會進行渲染等原生操做。假如每執行一個異步操做都要從新生成一個 task,將提升宿主平臺的負擔和響應時間。因此,須要有一個概念,在進行下一個 task 以前,將當前 task 生成的低延遲的,與下一個 task 無關的異步操做執行完,這就是 microtask。

這裏的 Quick Sort Demo 展現了 microtask 和 task 在延遲執行上的巨大區別。

對於在不通宿主環境中選擇合適的 microtask,能夠選擇 asapsetImmediate 的代碼做爲參考。

Promise 的中的同步與異步

new Promise((resolve) => {
  console.log('a')
  resolve('b')
  console.log('c')
}).then((data) => {
  console.log(data)
})

// a, c, b複製代碼

使用過 Promise 的人都知道輸出 a, c, b,但有多少人能夠清楚地說出從建立 Promise 對象到執行完回調的過程?下面是一個完整的解釋:

構造函數中的輸出執行是同步的,輸出 a, 執行 resolve 函數,將 Promise 對象狀態置爲 resolved,輸出 c。同時註冊這個 Promise 對象的回調 then 函數。整個腳本執行完,stack 清空。event loop 檢查到 stack 爲空,再檢查 microtask 隊列中是否有任務,發現了 Promise 對象的 then 回調函數產生的 microtask,推入 stack,執行。輸出 b,event loop的列隊爲空,stack 爲空,腳本執行完畢。

以基礎的 Promises/A+ 規範爲範本

規範地址:

值得注意的是:

Finally, the core Promises/A+ specification does not deal with how to create, fulfill, or reject promises, choosing instead to focus on providing an interoperable then method. Future work in companion specifications may touch on these subjects.

Promises/A+ 規範主要是制定一個通用的回調方法 then,使得各個實現的版本能夠造成鏈式結構進行回調。這使得不一樣的 Promise 庫內部細節實現可能不同,可是隻有具備想通的 then 方法,返回的 Promise API 之間就能夠相互調用。

下面會實現一個簡單的 Promise,不想看實現的能夠跳過。項目地址在這裏,歡迎更多討論。

Promise 構造函數,選擇平臺的 microtask 實現

// Simply choose a microtask
const asyncFn = function() {
  if (typeof process === 'object' && process !== null && typeof(process.nextTick) === 'function')
    return process.nextTick
  if (typeof(setImmediate === 'function'))
    return setImmediate
  return setTimeout
}()

// States
const PENDING = 'PENDING'

const RESOLVED = 'RESOLVED'

const REJECTED = 'REJECTED'

// Constructor
function MimiPromise(executor) {
  this.state = PENDING
  this.executedData = undefined
  this.multiPromise2 = []

  resolve = (value) => {
    settlePromise(this, RESOLVED, value)
  }

  reject = (reason) => {
    settlePromise(this, REJECTED, reason)
  }

  executor(resolve, reject)
}複製代碼

stateexecutedData 都容易理解,可是必需要理解一下爲何要維護一個 multiPromise2 數組。因爲規範中說明,每一個調用過 then 方法的 promise 對象必須返回一個新的 promise2 對象,因此最好的方法是當調用 then 方法的時候將一個屬於這個 then 方法的 promise2 加入隊列,在 promise 對象中維護這些新的 promise2 的狀態。

  • executorpromise 構造函數的執行函數參數
  • statepromise 的狀態
  • multiPromise2:維護的每一個註冊 then 方法須要返回的新 promise2
  • resolve:函數定義了將對象設置爲 RESOLVED 的過程
  • reject:函數定義了將對象設置爲 REJECTED 的過程

最後執行構造函數 executor,並調用 promise 內部的私有方法 resolvereject

settlePromise 如何將一個新建的 Promise settled

function settlePromise(promise, executedState, executedData) {
  if (promise.state !== PENDING)
    return

  promise.state = executedState
  promise.executedData = executedData

  if (promise.multiPromise2.length > 0) {
    const callbackType = executedState === RESOLVED ? "resolvedCallback" : "rejectedCallback"

    for (promise2 of promise.multiPromise2) {
      asyncProcessCallback(promise, promise2, promise2[callbackType])
    }
  }
}複製代碼

第一個判斷條件很重要,由於 Promise 的狀態是不可逆的。在 settlePromise 的過程當中假如狀態不是 PENDING,則不須要繼續執行下去。

當前 settlePromise 的環境,能夠有三種狀況:

  • 異步延遲執行 settlePromise 方法,線程已經同步註冊好 then 方法,須要執行全部註冊的 then 回調函數
  • 同步執行 settlePromise 方法,then 方法未執行,後面須要執行的 then 方法會在註冊的過程當中直接執行
  • 不管執行異步 settlePromise 仍是同步 settlePromise 方法,並無註冊的 then 方法須要執行,只須要將本 Promise 對象的狀態設置好便可

then 方法的註冊和當即執行

MimiPromise.prototype.then = function(resolvedCallback, rejectedCallback) {
  let promise2 = new MimiPromise(() => {})

  if (typeof resolvedCallback === "function") {
      promise2.resolvedCallback = resolvedCallback;
  }
  if (typeof rejectedCallback === "function") {
      promise2.rejectedCallback = rejectedCallback;
  }

  if (this.state === PENDING) {
    this.multiPromise2.push(promise2)
  } else if (this.state === RESOLVED) {
    asyncProcessCallback(this, promise2, promise2.resolvedCallback)
  } else if (this.state === REJECTED) {
    asyncProcessCallback(this, promise2, promise2.rejectedCallback)
  }

  return promise2
}複製代碼

每一個註冊 then 方法都須要返回一個新的 promise2 對象,根據當前 promise 對象的 state,會出現三種狀況:

  • 當前 promise 對象處於 PENDING 狀態。構造函數異步執行了 settlePromise 方法,須要將這個 then 方法對應返回的 promise2 放入當前 promisemultiPromise2 隊列當中,返回這個 promise2。之後當 settlePromise 方法異步執行的時候,執行所有註冊的 then 回調方法
  • 當前 promise 對象處於 RESOLVED 狀態。構造函數同步執行了 settlePromise 方法,直接執行 then 註冊的回調方法,返回 promise2
  • 當前 promise 對象處於 REJECTED 狀態。構造函數同步執行了 settlePromise 方法,直接執行 then 註冊的回調方法,返回 promise2

異步執行回調函數

function asyncProcessCallback(promise, promise2, callback) {
  asyncFn(() => {
    if (!callback) {
      settlePromise(promise2, promise.state, promise.executedData);
      return;
    }

    let x

    try {
      x = callback(promise.executedData)
    } catch (e) {
      settlePromise(promise2, REJECTED, e)
      return
    }

    settleWithX(promise2, x)
  })
}複製代碼

這裏用到咱們以前選取的平臺異步執行函數,異步執行 callback。假如 callback 沒有定義,則將返回 promise2 的狀態轉換爲當前 promise 的狀態。而後將 callback 執行。最後再 settleWithX promise2 與 callback 返回的對象 x

最後的 settleWithX 和 settleXthen

function settleWithX (p, x) {
    if (x === p && x) {
        settlePromise(p, REJECTED, new TypeError("promise_circular_chain"));
        return;
    }

    var xthen, type = typeof x;
    if (x !== null && (type === "function" || type === "object")) {
        try {
            xthen = x.then;
        } catch (err) {
            settlePromise(p, REJECTED, err);
            return;
        }
        if (typeof xthen === "function") {
            settleXthen(p, x, xthen);
        } else {
            settlePromise(p, RESOLVED, x);
        }
    } else {
        settlePromise(p, RESOLVED, x);
    }
    return p;
}

function settleXthen (p, x, xthen) {
    try {
        xthen.call(x, function (y) {
            if (!x) return;
            x = null;

            settleWithX(p, y);
        }, function (r) {
            if (!x) return;
            x = null;

            settlePromise(p, REJECTED, r);
        });
    } catch (err) {
        if (x) {
            settlePromise(p, REJECTED, err);
            x = null;
        }
    }
}複製代碼

這裏的兩個方法對應 Promise/A+ 規範裏的第三章,因爲實在太囉嗦,這裏就再也不過多解釋了。

配合 async/await 使用更加美味

V8 已經原生實現了 async/await,Node 和各瀏覽器引擎的實現也會慢慢跟進,而 babel 早就加入了 async/await。目前客戶端仍是用 babel 預編譯使用比較好,而 Node 須要升級到 v7 版本,而且加入 --harmony-async-await 參數。

Promise 其中的一個侷限在於:全部操做過程都必須包含在構造函數或者 then 回調中執行,假若有一些變量須要累積向下鏈式使用,還要加入外部全局變量,或者引發回調地獄,像這樣。

let result1
let result2
let result3

getSomething1()
  .then((data) => {
    result1 = data
    // do some shit with result1
    return getSomething2()
  })
  .then((data) => {
    result2 = data
    // do some other shit with result1 and result2
    return getSomething3()
  })
  .then((data) => {
    result3 = data
    // do some other shit with result1, result2 and result3
  })
  .catch((err) => {
    console.error(err);
  })

getSomething1()
  .then((data1) => {
    // do some shit with data1
    return getSomething2()
    .then((data2) => {
      // do some shit with data1 and data2
      return getSomething3()
      .then((data3) => {
        // do some shit with data1, data2 and data3
      })
    })
  })
  .catch((err) => {
    console.error(err);
  })複製代碼

引入了全局變量和寫出了回調地獄都不是明智的作法,假如用了 async/await,能夠這樣:

async function a() {
  try {
    const result1 = await getSomething1()
    // do some shit with result1
    const result2 = await getSomething2()
    // do some other shit with result1 and result2
    const result3 = await getSomething3()
    // do some other shit with result1, result2 and result3
  } catch (e) {
    console.error(e);
  }
}複製代碼

async/await 配合 Promise,沒有了 then 方法和回調地獄的寫法是否是清爽了不少?

結語

本文後續其實還有更多值得挖掘的地方:

  • 如何更加有效地選取平臺的 microtask?
  • 如何實現一個可用的符合 ECMAScript 規範的 Promise?
  • microtask 和 task 在 event loop 具體的執行過程?

能夠期待後續的更多內容。最後再貼一下項目地址,歡迎繼續的討論。

參考資料

相關文章
相關標籤/搜索