前言
前幾天在專案中遇到回撥地獄的情況,但我在剛開始寫這個專案的時候還不會用Promise,只知道它可以用來解決回撥地獄,所以就用全都用回撥函式解決。我是看《深入理解ES6》這本書來學Promise的,在用法和功能上講得還是挺詳細的,但在原理上沒怎麼講,所以用的時候有很多地方有疑問,於是在網上找到一篇自己實現Promise的文章來理解Promise實現原理。
參考連結:www.mattgreer.org/articles/pr…
一、最簡單的用法
new Promise( (resolve, reject) => {
resolve(42)
}).then( result => {
console.log(result)
})
// 控制檯輸出
// 42
複製程式碼
這是一段最簡單的Promise使用(我們先不討論拒絕reject
,假設所有的操作都被接受resolve
),很開心,終於會用Promise了(心虛~),但我在使用的時候出現了一些疑問,在我說我的疑問之前,我們先說一下函式的回撥
回撥函式
在js中,函式a作為引數傳給另一個函式b,此時函式a就叫做回撥函式。在函式b的函式體中呼叫傳進來的函式a,此時函式a就被函式b回撥了。
OK,現在我們懂什麼是回撥函式了,我可以說我的疑問了
二、我的疑問 —— resolve
和 then
首先我們先分析一下上面最簡單的例子中有多少個回撥函式,我把上面的程式碼重新寫一下:
// 現在不用箭頭函式,用變數直觀點,這裡就不寫reject了
let fn1 = function (resolve) {
resolve(42)
}
let fn2 = function (result) {
console.log(result)
}
new Promise(fn1).then(fn2) // 42
複製程式碼
從 new Promise(fn1).then(fn2)
這行程式碼可以很直觀地看出這裡有 2 個回撥函式,但還沒完,再看看fn1
這個函式
let fn1 = function (resolve) {
// resolve再這裡被呼叫了,說明resolve是個回撥函式
resolve(42)
}
複製程式碼
所以現在我們的會調函式是這樣的:
在new Promise
的時候給Promise傳入了一個含有resolve
這個回撥函式的回撥函式fn1
,建立完Promise物件之後,隨即呼叫了Promise物件中的then()
方法,給then
方法傳入了一個回撥函式fn2
。(這裡涉及到一點點的new
操作符的作用)
好了,分析完回撥函式之後我開始想不通了:
- 疑問1:什麼時候呼叫
fn1
我都不知道 - 疑問2:我沒有給
fn1
傳回撥函式啊 - 疑問3:就先裝作我知道
fn1
是什麼時候呼叫的吧,那為什麼在fn1
呼叫resolve
的時候,為什麼看起來像呼叫的是我傳過去給then
的回撥函式fn2
呢 - 啊?
三、實現原理
終於回到正題了,解決一下我上面的疑問。
在開始的那篇參考的文章中,作者實現了一個還算比較完整的Promise,內容有點長,我就只擷取其中能夠實現上面寫的最簡單Promise使用的原理。
function Promise(fn) {
var state = 'pending'; // Promise當前的狀態
var value; // 當Promise處於接受狀態時儲存下來的值,這是結果
var deferred; // 延期執行函式(現在還看不出來它是函式)
function resolve(newValue) { // Promise被接受之後執行的函式
value = newValue; // 儲存獲取到的結果
state = 'resolved'; // 將狀態改為resolved
if(deferred) { // 如果deffred不是undefined,已經有指向函式的引用
handle(deferred); // 將deffred傳給handle函式處理
}
}
/*
* 當使用者呼叫then函式時呼叫的處理函式
* 等等就解釋
*/
function handle(onResolved) {
if(state === 'pending') { // 判斷當前所處的狀態,如果是padding狀態,即沒有被處理狀態,還處於初始狀態
deferred = onResolved; // 將onResolved回撥函式儲存下來,稍後執行
return; // padding狀態就不執行下面的語句
}
onResolved(value); // 如果不是padding狀態,呼叫onResolved,並把結果傳給回撥函式
}
/*
* 這個是我的疑問then了
* 這裡涉及一個關鍵字this,涉及的內容有點多,先不說
* 看等號後面是一個function,所以它是一個函式,並且傳了一個引數,是回撥函式
* 從上面的的程式碼可以看出onResolved是一個回撥函式
*/
this.then = function(onResolved) {
handle(onResolved); // 每當呼叫then這個函式,就將onResolved(例子中的fn2)傳給handle函式
};
/*
* 上面全都是函式和變數的定義,只有這一行是執行函式
* 疑問1解決:在new Promise的時候就執行了傳過來的回撥函式fn1
* 疑問2解決:在呼叫fn1的時候傳過去的回撥函式是Promise的內部函式resolve
* 疑問3解決:有點繞,我寫在程式碼外面
*/
fn(resolve);
}
複製程式碼
疑問三解決:
前兩個疑問的解決請看fn
的註釋
我們在用Promise物件呼叫then函式的時候給他傳了fn2
,也就是onResolved
是指向fn2
的,然後onResolved
又傳給了handle
,在handle
裡面呼叫了onResolved
,也就是說,fn2
在handle
裡呼叫。
現在我們知道了fn2
是在handle
裡被呼叫了。然後看handle
在哪裡被呼叫:
因為例子中沒有非同步操作,所以先不考慮padding情況:
handle
函式在then
函式中呼叫
再看回最簡單的Promise用法:
let fn1 = function (resolve) {
resolve(42)
}
let fn2 = function (result) {
console.log(result)
}
new Promise(fn1).then(fn2) // 42
複製程式碼
1. 執行new Promise(fn1)
時做了什麼?
當程式執行到
new Promise
時,Promise
裡面的語句fn(resolve)
會馬上執行,呼叫了我們傳過去的回撥函式fn1
,並且給我們傳回一個值叫resolve,這個值恰好又是一個回撥函式(引數名resolve的指向是Promise
物件中的resolve()
函式,我們在這個例子中沒有寫任何的非同步操作,所以東西幾乎都是同步執行的)。我們又馬上呼叫了resolve
回撥函式,並給它傳入一個值42(newValue),state
被置為'resolved',此時new Promise
就全部執行完成。總結一下上面這段話:我們這個例子沒有非同步操作,所以resolve
函式在new Promise
的時候就被呼叫了完了,狀態改成了'resolved'
2. 執行then(fn2)
時做了什麼?
例子中建立完
Promise
物件之後馬上呼叫了then
函式,並且傳入了一個會調函式fn2
(這裡的then
其實是一個微任務,這裡先不做討論,暫且當它是馬上執行的)。呼叫then
時,then
呼叫了Promise
物件的函式handle
並且把onResolved指向了fn2
,因為在new Promise
的時候就已經把status
改成'resolved'了,所以handle
函式體裡面的if
語句不成立,於是就執行onResolved(value)
並且把new Promise
中的到的值傳入,也就是說fn2
被handle
回撥了。總結上面的這段話:在我們呼叫then
的時候呼叫了handle
,並且把new Promise
時得到的值傳回給fn2回撥函式
到現在疑問三解決了。
總結疑問三:
fn
被呼叫的時候,給fn1
傳回了Promise
的內部函式resolve
,被fn1
的resolve引數所接收,所以在fn1
方法體內呼叫的resolve
實際是指向Promise
的內部函式resolve
,並未我們給他傳了42這個值,被Promise
的resolve
函式儲存在value當中。在呼叫then
函式的時候,value被handle
傳出來給fn2
。
終於解決完問題了!!
關於Promise的三種狀態的作用,還有如何實現序列的Promise,和reject,看完那篇參考文章的還是很好理解的,裡面還有很多東西,我就不寫了,去看吧。
四、Promise 的使用和理解中涉及到的知識點
- new 關鍵字建立物件
- this關鍵字
- 原型鏈
- js和瀏覽器的非同步機制
- event loop