引言
Promise
是一種非同步程式設計的解決方案,通過鏈式呼叫的方式解決回撥地獄。作為前端面試中的考點,也是前端的基本功,掌握其原理是非常重要的。本次分享就從Promise
的使用方式上出發,一步一步剖析其原理,最後幫助大家封裝出自己的Promise
。
注:如果你還不瞭解Promise
,建議點選這裡學習Promise
的基本使用語法。
本文全長2665個字,閱讀8分鐘你將收穫:
Promise
的原理- 構建自己的
Promise
函式的能力
閱讀60分鐘你將徹底明白Promise
的原理。
正文
知其然才能知其所以然,我們先來看一下最常使用的例子,分析有什麼特徵。
使用例子
熟悉Promise
使用的人都知道,當呼叫getNews()
返回一個新的Promise
時,裡面的非同步呼叫操作將會立刻執行,然後在非同步回撥裡呼叫resolve
方法和reject
方法改變Promise
的狀態,執行對應的then
或者catch
函式。
以上程式碼有以下幾個特徵:
Promise
是一個建構函式,其接受一個函式作為引數。- 作為引數的函式裡,有兩個方法
resolve
和reject
Promise
帶有方法then
和catch
那resolve
和reject
是怎麼來的?Promise
例項化的時候都做了什麼?
別急,所謂生死看淡,不服就幹。在回答這兩個問題之前,我們可以先直接嘗試構建自己的Promise
開始構建
- 建構函式
fn
就是我們使用時傳入的回撥函式。
resolve
和reject
那麼fn
是什麼時候呼叫的呢?其實,在Promise
例項初始化的時候內部就自動呼叫了,並且傳入了內部的resolve
和reject
方法給開發者呼叫,就像下面這樣:
resolve
和reject
是Promise
內部提供給開發者的。
- 新增
then
和catch
方法
這兩個方法是Promise
例項的方法,因此應該寫在this
或者prototype
上。
Promise
基本的骨架就出來了,下面我們仔細嘮嘮這4個函式的具體作用。
作用分析
resolve
和reject
想象一下我們日常使用Promise
的場景,在非同步請求之後,是需要我們手動呼叫resolve
或reject
方法去改變狀態的。
resolve
呼叫意味著非同步請求已經有了結果,可以執行then
裡面的回撥了(reject
同理,非同步請求失敗時候執行catch
函式。)
then
和catch
呼叫上述的函式時如下:
我們知道,在resolve
被呼叫前,then
和catch
函式裡面的回撥是不會執行的。那麼我們這樣寫的時候,它做了什麼呢?
實際上,Promise
悄悄把我們寫的回撥函式儲存了起來,等到我們手動呼叫resolve
或者reject
時才依次去執行。也就是說,Promise
裡的then
和catch
的作用就是:註冊回撥函式,先把一系列的回撥函式存起來,等到開發者呼叫的時候才拿出來執行。
所以,then
和 catch
函式應該長這樣:
resolve
和reject
就是分別去呼叫他們而已。
到目前為止可以回答第二個問題了:Promise
初始化時,內部呼叫了我們傳入的函式,並將resolve
和reject
方法作為引數提供。在之後呼叫的then
或者catch
方法裡,把回撥函式儲存在內部的一個佇列中,等待狀態改變時候呼叫。
鏈式呼叫
接下來我們實現普通的鏈式呼叫,實現鏈式呼叫非常簡單。由於then
是掛載到this
上的方法,如果我們在then
中直接返回this
就可以實現鏈式呼叫了。就像這樣:
then
函式返回的還是例項物件本身,所以就可以一直呼叫then
方法。同時okCallback
應該變成一個陣列,才能儲存多次呼叫then
方法的回撥。而當okCallback
是一個陣列時,呼叫resolve方法就需要遍歷okCallback
,依次呼叫,就像下面這樣:
在resolve
中,每次呼叫函式的返回值將會成為下一個函式的引數。以此就可以進行then
回撥的引數傳遞了。
狀態引入和延時機制
在Promise
規範裡,最初的狀態是pending
,當呼叫resolve
之後轉變為fulfilled
,而呼叫reject
之後轉變為rejected
。狀態只能轉變一次。
另外,為了保證resolve
呼叫時,then
已經全部註冊完畢,還應該引入setTimeout
延遲resolve
的執行:
鏈式呼叫Promise(重點)
實際開發中,經常會遇到有前後順序要求的非同步請求,我們往往會在then
回撥裡返回一個Promise
例項,這意味著我們需要等待這個新的Promise
例項resolve
之後才能繼續進行下面的then
呼叫。
MyPromise
顯然不能支援這種場景,那麼怎麼才能實現這個執行權的交替呢?
有一個很簡單的方法,還記得then
函式的作用嗎?
對,註冊回撥。
既然它返回了一個新的Promise
導致我們不能正常執行後續的then
回撥,那我們直接把後續的then
回撥全部轉移到這個新的Promise
上,讓它代替執行不就好了嗎?
Promise
例項時,直接呼叫新例項的then
方法註冊剩餘的回撥,然後直接return
,等到新例項resolve
時,就會繼續代替執行剩下的then
回撥了。
完了嗎?完了,到這裡已經能夠實現Promise
的鏈式呼叫了,也就說今天的8分鐘你已經可以寫出自己的Promise
了,恭喜!
不過,這種做法並非Promise
標準,想知道在Promise
標準裡是怎麼解決執行權的轉交問題嗎?也不復雜,但是需要你有非常好的耐心去仔細理解裡面的邏輯,準備好了就接著往下看吧~
Promises/A+標準下的鏈式呼叫
(為了簡化模型,這裡我們只分析resolve部分的邏輯,這將涉及到3個函式:then
,handle
和resolve
)
then
:此時then
函式不再返回this
,而是直接返回一個全新的Promise
,這個Promise
就是連線兩個then
之間的橋樑。
handle
:當狀態為pending
時,註冊回撥。否則直接呼叫。
resolve
:嘗試遍歷執行註冊的回撥。如果引數是一個promise
例項,則將執行權移交給新的promise
,自身暫停執行。
看到這裡是不是覺得很複雜?其實也不復雜,一句話就可以概括:
在promises/A+規範裡,後一個promise儲存了前一個promise 的resolve引用。
前一個resolve
會帶動後一個resolve
,當resolve
的引數是Promise
例項時,暫停自身resolve
的呼叫,把自身作為引用傳遞給新的Promise
例項,新的Promise
例項的resolve
會引起自身resolve
。
如果還不理解的話,我們可以實際看一個例子。(請一邊看例子,一邊對照著程式碼思考哦,程式碼在最後附錄,建議貼上到本地編輯器對照著思考。)
比基尼海灘的海綿寶寶想要外賣一個蟹黃堡,他必須首先上網查到蟹堡王的外賣電話,然後才能點外賣。用JavaScript描述就是下面這樣:
在最開始的階段,一共會生成3個promise
,分別是getPhoneNumber()
,第一個then()
和第二個then()
(getHamburger
還未呼叫,因此沒有計算在內)
讓我們來看看對應的程式碼執行吧。現在是註冊回撥階段,第一個then
返回的promise
將會把自身的resolve
引用傳遞給getPhoneNumber()
的handle
函式,而handle
函式會同時把resolve
應用和對應的then
回撥一同儲存起來:
第二個then
同理,所以註冊階段結束後,各個promise
內部的狀態如下圖所示:
在呼叫階段,會隨著getPhoneNumber()
的resolve
引發後續的resolve
,整個過程可以用下圖表示:
- 首先,
getPhoneNumber()
觸發resolve
,返回值是number
,因此可以遍歷呼叫callbacks
裡儲存的回撥。 - 進入
handle
函式,改變getPhoneNumber()
的state
,之後函式①被呼叫,該函式返回getHamburger()
。呼叫第一個then
的resolve
引用,並將getHamburger()
作為引數傳遞。 - 視線轉移到第一個
then()
。在判斷引數getHamburder()
是一個Promise
例項之後,將自身的resolve
作為回撥,呼叫其then
方法(可以看到上圖中,getHamburger
的callbacks
裡儲存的其實是前一個then
的resolve
引用,因為此時前面的Promise
被中斷了,因此當開發者呼叫getHamburger()
的resolve
方法時,才能繼續未完成的resolve
執行。) - 等到
getHamburger()
的resolve
呼叫時,實際上就會呼叫上一個then
的resolve
,返回值作為引數傳遞給右邊的then
,使其resolve
- 視線再一次到第一個
then()
上。進入自身的handle
方法,改變state
,之後函式②被呼叫,觸發第二個then()
(也就是上圖最右邊)的resolve
- 最後,呼叫最後一個
then()
的resolve
(也就是上圖中的小then()
,這個then()
是程式碼自動呼叫生成的。),整個非同步過程結束。
總結
Promise
使用一個resolve
函式讓我們不用面臨回撥地獄(因為回撥已經通過鏈式的方式註冊儲存起來了),實際上做的就是一層封裝。其中最難理解的部分就是Promise
的鏈式呼叫。本次跟大家分享的第一種方式非常簡單粗暴,即把未執行完的回撥轉交給下一個Promise
即可。第二種方式本著不拋棄不放棄的原則,多個then
函式通過resolve
引用連成一氣,前面的resolve
將可能會引起後面一系列的resolve
,頗有多米諾骨牌的感覺。
附錄
Promises/A+ 規範程式碼示例(來源:參考[1])
function MyPromise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
return new MyPromise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
})
})
}
let handle = (callback) => {
if (state === 'pending') {
callbacks.push(callback)
return
}
//如果then中沒有傳遞任何東西
if(!callback.onFulfilled) {
callback.resolve(value)
return
}
var ret = callback.onFulfilled(value)
callback.resolve(ret)
}
let resolve = (newValue) => {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then
if (typeof then === 'function') {
then.call(newValue, resolve)
return
}
}
state = 'fulfilled'
value = newValue
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback)
})
}, 0)
}
fn(resolve)
}
複製程式碼
參考
- 30分鐘,讓你徹底明白Promise原理 mengera88 2017-05-19
- 深入淺出Nodejs 樸靈 P90