背景
在上一篇博客[[譯]前端基礎知識儲備——Promise/A+規範](https://segmentfault.com/a/11...,我們介紹了Promise/A+規範的具體條目。在本文中,我們來選擇了promiz,讓大家來看下一個具體的Promise庫的內部代碼是如何運作的。
promiz是一個體積很小的promise庫(官方介紹約爲913 bytes (gzip)),作爲一個ES2015標準中的Promise的polyfill,實現了諸如resolve
、all
和race
等API。
知識儲備
我們在這裏簡單回顧一下Promise/A+的主要關鍵點,如果需要了解詳細內容的同學,可以閱讀我的上一篇博客。
- Promise有三個狀態,分別爲
pending
、fulfilled
和rejected
,且只能從pending
到fulfilled
或者rejected
,沒有其他的流轉方式。 - Promise的返回值是一個新的Promise,原因見上一條。
- 傳遞給
then
函數的兩個回調函數,有且僅有一次機會被執行(即執行了onfulfilled
就不會執行onrejected
函數,且只執行一次)。
代碼實現與分析
異步執行器
在介紹Promise之前,我們先介紹一下異步執行器。在Promise中,我們需要一個異步的執行器來異步執行我們的回調函數。在規範中提到,通常情況下,我們可以使用微任務(nextTick)或者宏任務(setTimeout)來實現。但是,如果我們需要兼容Web Worker這種情況的話,我們可能還需要一些更多的方式來處理。具體代碼如下:
var queueId = 1
var queue = {}
var isRunningTask = false
// 使用postMessage來執行異步函數
if (!global.setImmediate)
global.addEventListener('message', function (e) {
if (e.source == global) {
if (isRunningTask)
nextTick(queue[e.data])
else {
isRunningTask = true
try {
queue[e.data]()
} catch (e) {}
delete queue[e.data]
isRunningTask = false
}
}
})
/**
* 異步執行方法
* @param {function} fn 需要執行的回調函數
*/
function nextTick(fn) {
if (global.setImmediate) setImmediate(fn)
// 如果在Web Worker中使用以下方法
else if (global.importScripts) setTimeout(fn)
else {
queueId++
queue[queueId] = fn
global.postMessage(queueId, '*')
}
}
以上代碼比較簡單,我們簡單說明下:
-
在代碼中,promiz使用了
setImmediate
、setTimeout
和postMessage
這三個方法來執行異步函數,其中:-
setImmedeate
,只有IE實現了該方法,在執行完隊列中的代碼後立即執行。 -
PostMessage
,新增的H5中的方法。 -
setTimeout
,兼容性最佳,可以適用各種場景。
-
因此,在promiz的這段代碼中,有一定的兼容性問題,應該把setTimeout放到最後作爲一個兜底策略,否則無法在老瀏覽器中執行。
構造函數
說完了異步函數執行器,我們來看下promise的構造函數。
首先我們來看下內存數據,我們需要存儲當前promise的狀態、成功的值或者失敗的原因、下一個promise的引用和成功與失敗的回調函數。因此,我們需要以下變量:
// states
// 0: pending
// 1: resolving
// 2: rejecting
// 3: resolved
// 4: rejected
var self = this,
state = 0, // promise狀態
val = 0, // success callback返回值
next = [], // 返回的新的promise對象
fn, er; // then方法中的成功回調函數和失敗回調函數
在存儲完相關數據後,我們來看下構造函數。
function Deferred(resolver) {
...
self = this;
try {
if (typeof resolver == 'function')
resolver(self['resolve'], self['reject'])
} catch (e) {
self['reject'](e)
}
}
構造函數非常簡單,除了聲明相關的函數,就只有執行傳入的callback而已。當然,如果我們不是鏈式調用的第一個promise,那麼我們會沒有resolver
參數,因此不需要在此執行,我們會在then
函數執行resolve
方法。
下面我們來看下上面提到的處理函數resovle
和reject
。
self['resolve'] = function (v) {
fn = self.fn
er = self.er
if (!state) {
val = v
state = 1
nextTick(fire)
}
return self
}
self['reject'] = function (v) {
fn = self.fn
er = self.er
if (!state) {
val = v
state = 2
nextTick(fire)
}
return self
}
self['then'] = function (_fn, _er) {
if (!(this._d == 1))
throw TypeError()
var d = new Deferred()
d.fn = _fn
d.er = _er
if (state == 3) {
d.resolve(val)
}
else if (state == 4) {
d.reject(val)
}
else {
next.push(d)
}
return d
}
在resolve
和reject
這兩個函數中,都是改變了內部promise的狀態,給定了參數值,同時異步觸發了fire函數。而then
方法,則是生成了一個新的Deferred
對象,並且完成了相關的初始化(執行完then方法我們就會得到這個新生成的Deferred
對象,也就是一個新的Promise);當前一個promise到達resolved
狀態時,不需要等待則直接出發resolve方法,rejected
狀態時也一樣。那麼,讓我們來看下fire方法到底是做什麼的呢?
function fire() {
// 檢測是不是一個thenable對象
var ref;
try {
ref = val && val.then
} catch (e) {
val = e
state = 2
return fire()
}
thennable(ref, function () {
state = 1
fire()
}, function () {
state = 2
fire()
}, function () {
try {
if (state == 1 && typeof fn == 'function') {
val = fn(val)
}
else if (state == 2 && typeof er == 'function') {
val = er(val)
state = 1
}
} catch (e) {
val = e
return finish()
}
if (val == self) {
val = TypeError()
finish()
} else thennable(ref, function () {
finish(3)
}, finish, function () {
finish(state == 1 && 3)
})
})
}
從上面的代碼來看,fire函數只是判斷了ref是不是一個thenable對象,然後調用了thenable函數,傳遞了3個回調函數。那麼這些回調函數到底是做什麼用的呢?我們需要來看下thenable函數的實現代碼。
// ref:指向thenable對象的`then`函數
// cb, ec, cn : successCallback, failureCallback, notThennableCallback
function thennable(ref, cb, ec, cn) {
// Promises can be rejected with other promises, which should pass through
if (state == 2) {
return cn()
}
if ((typeof val == 'object' || typeof val == 'function') && typeof ref == 'function') {
try {
// cnt變量用來保證成功和失敗的回調函數總共只會被執行一次
var cnt = 0
ref.call(val, function (v) {
if (cnt++) return
val = v
cb()
}, function (v) {
if (cnt++) return
val = v
ec()
})
} catch (e) {
val = e
ec()
}
} else {
cn()
}
};
在thenable函數中,如果判斷當前的promise的狀態是處於rejecting
時,會直接執行cn
,也就是將reject狀態傳遞下去。而如果當ref不是一個thenable對象的then
函數時(那麼此時值爲undefined),那麼就會直接執行cn
。
通過fire函數傳遞的三個callback我們可以看到,cn
是在promise的狀態改變時,針對特定的狀態來觸發相對應的onfulfilled
或者onrejected
回調函數。
只有當ref
是一個thenable
時(傳遞給resolve
的是一個promise),代碼纔會進入上面的try catch
邏輯中。
Promise執行流程
看完了上面的各部分代碼,我相信大家可能對整個執行流程仍然不夠熟悉,下面,我們將這些流程拼接起來,通過幾個完整的流程來說明下。
鏈式調用第一個Promise
當我們聲明一個promise式,我們會傳入一個resolver
。此時,整個Deferred
對象的state
是0。如果我們在resolver
裏面調用了resolve
方法,那麼我們的state
就會變成1,然後出發fire函數註冊到thenable函數裏面的第三個回調函數,從而將值傳遞給下一個thenable。當thenable的then函數執行完成(即我們看到的Promise後面跟着的then函數執行完成以後),我們的state
纔會變成3,也就是說上一個Promise纔會結束,返回一個新的Promise。
鏈式調用非第一個Promise
如果不是第一個Promise,那麼我們就沒有resolver
參數。因此,我們的resolve
方法並不是通過在resolver
中進行調用的,而是將回調函數fn
註冊進來,在上一個Promise完成後主動調用執行的。也就是說,我們在上一個Promise執行完then函數並且返回一個新的Promise時,我們這個返回的Promise就已經進入了resolving
的狀態。
給resolve
傳遞一個Promise
在Promise/A+規範中,如果我們給resolve
傳遞一個promise,那麼我們的通過resolve
獲取到的值就是傳遞進去的這個promise返回的值。當然,我們也必須等待作爲參數的這個promise處理完成後,纔會處理外面的這個promise。
在promiz的代碼中,我們如果通過resolve
接收到一個promise,那麼我們在fire函數中就會吧promise.then
的引用傳遞給thenable
函數。在thenable
函數中,我們會將我們當前promise需要執行的onfulfilled
和onrejected
封裝成一個函數,傳遞給作爲參數的promise的then函數。因此,當作爲參數的promise執行任意結果的回調函數時,就會將參數傳遞給外層的promise,執行對應的回調函數。
全局執行方法
Promise.all
讓我們先看代碼。
Deferred.all = function (arr) {
if (!(this._d == 1))
throw TypeError()
if (!(arr instanceof Array))
return Deferred.reject(TypeError())
var d = new Deferred()
function done(e, v) {
if (v)
return d.resolve(v)
if (e)
return d.reject(e)
var unresolved = arr.reduce(function (cnt, v) {
if (v && v.then)
return cnt + 1
return cnt
}, 0)
if (unresolved == 0)
d.resolve(arr)
arr.map(function (v, i) {
if (v && v.then)
v.then(function (r) {
arr[i] = r
done()
return r
}, done)
})
}
done()
return d
}
在Promise.all
中,我們使用了一個計數器來進行統計,在每一個Promise後面都增加一個then函數用於增加計數。當Promise成功時則計數+1。當整個數組中的Promise都已經進入resolved
狀態時,我們纔會執行thenable
的then函數。如果有一個失敗的話,則立即進入reject流程。
總結
從代碼設計層面來看,promiz的代碼量較少,閱讀也較爲簡單。但是,在某些細節的設計上,promiz還是體現出了較爲巧妙的思路,如在處理作爲入參的promise時,能夠在這個promise後面動態的添加一個then
函數,從而獲取數據給外面的promise。
如果大家有興趣,建議自己根據本文的說明閱讀一遍源碼,配合Promise/A+規範來看下是如何實現每一條規範的。
下一篇博客,我們將爲大家從頭開始,來實現一個Promise庫。