原文地址javascript
從歷史的進程來看,Javascript 異步操做的基本單位已經從 callback 轉換到 Promise。除了在特殊場景使用 stream,RxJs 等更加抽象和高級的異步數據獲取和操做流程方式外,如今幾乎任何成熟的異步操做庫,都會實現或者引用 Promise 做爲 API 的返回單位。主流的 Javascript 引擎也基本原生實現了 Promise。php
在 Promise 遠未流行之前,Javascript 的異步操做基本都在使用以 callback 爲主的異步接口。鼠標鍵盤事件,頁面渲染,網絡請求,文件請求等等異步操做的回調函數都是用 callback 來處理。隨着異步使用場景範圍的擴大,出現了大量工程化和富應用的的交互和操做,使得應用不足以用 callback 來面對越發複雜的需求,慢慢出現了許多優雅和先進的異步解決方案:EventEmitter
,Promise
,Web Worker
,Generator
,Async/Await
。html
目前 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 的人都知道,Promise 所說的異步執行,只是將 Promise 構造函數中 resolve
,reject
方法和註冊的 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.nextTick
,setImmediate
,postMessage
,MessageChannel
等
根據規範,microtask 存在的意義是:在當前 task 執行完,準備進行 I/O,repaint
,redraw
等原生操做以前,須要執行一些低延遲的異步操做,使得瀏覽器渲染和原生運算變得更加流暢。這裏的低延遲異步操做就是 microtask。原生的 setTimeout 就算是將延遲設置爲 0 也會有 4 ms 的延遲,會將一個完整的 task 放進隊列延遲執行,並且每一個 task 之間會進行渲染等原生操做。假如每執行一個異步操做都要從新生成一個 task,將提升宿主平臺的負擔和響應時間。因此,須要有一個概念,在進行下一個 task 以前,將當前 task 生成的低延遲的,與下一個 task 無關的異步操做執行完,這就是 microtask。
這裏的 Quick Sort Demo 展現了 microtask 和 task 在延遲執行上的巨大區別。
對於在不通宿主環境中選擇合適的 microtask,能夠選擇 asap 和 setImmediate 的代碼做爲參考。
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 爲空,腳本執行完畢。
規範地址:
值得注意的是:
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,不想看實現的能夠跳過。項目地址在這裏,歡迎更多討論。
// 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)
}複製代碼
state
和 executedData
都容易理解,可是必需要理解一下爲何要維護一個 multiPromise2
數組。因爲規範中說明,每一個調用過 then
方法的 promise
對象必須返回一個新的 promise2
對象,因此最好的方法是當調用 then
方法的時候將一個屬於這個 then
方法的 promise2
加入隊列,在 promise
對象中維護這些新的 promise2
的狀態。
executor
: promise
構造函數的執行函數參數state
:promise
的狀態multiPromise2
:維護的每一個註冊 then
方法須要返回的新 promise2
resolve
:函數定義了將對象設置爲 RESOLVED
的過程reject
:函數定義了將對象設置爲 REJECTED
的過程最後執行構造函數 executor
,並調用 promise
內部的私有方法 resolve
和 reject
。
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 對象的狀態設置好便可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
放入當前 promise
的 multiPromise2
隊列當中,返回這個 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
。
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+ 規範裏的第三章,因爲實在太囉嗦,這裏就再也不過多解釋了。
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
方法和回調地獄的寫法是否是清爽了不少?
本文後續其實還有更多值得挖掘的地方:
能夠期待後續的更多內容。最後再貼一下項目地址,歡迎繼續的討論。