小而美的Promise庫——promiz原始碼解析

黃Java發表於2018-11-01

背景

在上一篇部落格[譯]前端基礎知識儲備——Promise/A+規範中,我們介紹了Promise/A+規範的具體條目。在本文中,我們來選擇了promiz,讓大家來看下一個具體的Promise庫的內部程式碼是如何運作的。

promiz是一個體積很小的promise庫(官方介紹約為913 bytes (gzip)),作為一個ES2015標準中的Promise的polyfill,實現了諸如resolveallrace等API。

知識儲備

我們在這裡簡單回顧一下Promise/A+的主要關鍵點,如果需要了解詳細內容的同學,可以閱讀我的上一篇部落格。

  • Promise有三個狀態,分別為pendingfulfilledrejected,且只能從pendingfulfilled或者rejected,沒有其他的流轉方式。
  • Promise的返回值是一個新的Promise,原因見上一條。
  • 傳遞給then函式的兩個回撥函式,有且僅有一次機會被執行(即執行了onfulfilled就不會執行onrejected函式,且只執行一次)。

程式碼實現與分析

非同步執行器

在介紹Promise之前,我們先介紹一下非同步執行器。在Promise中,我們需要一個非同步的執行器來非同步執行我們的回撥函式。在規範中提到,通常情況下,我們可以使用微任務(nextTick)或者巨集任務(setTimeout)來實現。但是,如果我們需要相容Web Worker這種情況的話,我們可能還需要一些更多的方式來處理。具體程式碼如下:

var queueId = 1var 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使用了setImmediatesetTimeoutpostMessage這三個方法來執行非同步函式,其中:
    • setImmedeate,只有IE實現了該方法,在執行完佇列中的程式碼後立即執行。
    • PostMessage,新增的H5中的方法。
    • setTimeout,相容性最佳,可以適用各種場景。

因此,在promiz的這段程式碼中,有一定的相容性問題,應該把setTimeout放到最後作為一個兜底策略,否則無法在老瀏覽器中執行。

建構函式

說完了非同步函式執行器,我們來看下promise的建構函式。

首先我們來看下記憶體資料,我們需要儲存當前promise的狀態、成功的值或者失敗的原因、下一個promise的引用和成功與失敗的回撥函式。因此,我們需要以下變數:

// states// 0: pending// 1: resolving// 2: rejecting// 3: resolved// 4: rejectedvar 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方法。

下面我們來看下上面提到的處理函式resovlereject

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
}複製程式碼

resolvereject這兩個函式中,都是改變了內部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, notThennableCallbackfunction 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需要執行的onfulfilledonrejected封裝成一個函式,傳遞給作為引數的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庫。

來源:https://juejin.im/post/5bdaf5d95188257fa660c0d1

相關文章