30分鐘,讓你完全明白Promise原理

前言

前一陣子記錄了promise的一些常規用法,這篇文章再深刻一個層次,來分析分析promise的這種規則機制是如何實現的。ps:本文適合已經對promise的用法有所瞭解的人閱讀,若是對其用法還不是太瞭解,能夠移步個人上一篇博文git

本文的promise源碼是按照Promise/A+規範來編寫的(不想看英文版的移步Promise/A+規範中文翻譯github

引子

爲了讓你們更容易理解,咱們從一個場景開始講解,讓你們一步一步跟着思路思考,相信你必定會更容易看懂。設計模式

考慮下面一種獲取用戶id的請求處理數組

1
2
3
4
5
6
7
8
9
10
11
12
13
//例1
function getUserId() {
return new Promise(function(resolve) {
//異步請求
http.get(url, function(results) {
resolve(results.id)
})
})
}
 
getUserId().then( function(id) {
//一些處理
})

getUserId方法返回一個promise,能夠經過它的then方法註冊(注意註冊這個詞)在promise異步操做成功時執行的回調。這種執行方式,使得異步調用變得十分順手。promise

原理剖析

那麼相似這種功能的Promise怎麼實現呢?其實按照上面一句話,實現一個最基礎的雛形仍是很easy的。異步

極簡promise雛形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Promise(fn) {
var value = null,
callbacks = []; //callbacks爲數組,由於可能同時有不少個回調
 
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};
 
function resolve(value) {
callbacks.forEach( function (callback) {
callback(value);
});
}
 
fn(resolve);
}

上述代碼很簡單,大體的邏輯是這樣的:函數

  1. 調用then方法,將想要在Promise異步操做成功時執行的回調放入callbacks隊列,其實也就是註冊回調函數,能夠向觀察者模式方向思考;
  2. 建立Promise實例時傳入的函數會被賦予一個函數類型的參數,即resolve,它接收一個參數value,表明異步操做返回的結果,當一步操做執行成功後,用戶會調用resolve方法,這時候其實真正執行的操做是將callbacks隊列中的回調一一執行;

能夠結合例1中的代碼來看,首先new Promise時,傳給promise的函數發送異步請求,接着調用promise對象的then屬性,註冊請求成功的回調函數,而後當異步請求發送成功時,調用resolve(results.id)方法, 該方法執行then方法註冊的回調數組。ui

相信仔細的人應該能夠看出來,then方法應該可以鏈式調用,可是上面的最基礎簡單的版本顯然沒法支持鏈式調用。想讓then方法支持鏈式調用,其實也是很簡單的:this

1
2
3
4
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};

see?只要簡單一句話就能夠實現相似下面的鏈式調用:url

1
2
3
4
5
6
// 例2
getUserId().then( function (id) {
// 一些處理
}).then( function (id) {
// 一些處理
});

加入延時機制

細心的同窗應該發現,上述代碼可能還存在一個問題:若是在then方法註冊回調以前,resolve函數就執行了,怎麼辦?好比promise內部的函數是同步函數:

1
2
3
4
5
6
7
8
9
// 例3
function getUserId() {
return new Promise(function (resolve) {
resolve( 9876);
});
}
getUserId().then( function (id) {
// 一些處理
});

這顯然是不容許的,Promises/A+規範明確要求回調須要經過異步方式執行,用以保證一致可靠的執行順序。所以咱們要加入一些處理,保證在resolve執行以前,then方法已經註冊完全部的回調。咱們能夠這樣改造下resolve函數:

1
2
3
4
5
6
7
function resolve(value) {
setTimeout( function() {
callbacks.forEach( function (callback) {
callback(value);
});
}, 0)
}

上述代碼的思路也很簡單,就是經過setTimeout機制,將resolve中執行回調的邏輯放置到JS任務隊列末尾,以保證在resolve執行時,then方法的回調函數已經註冊完成.

可是,這樣好像還存在一個問題,能夠細想一下:若是Promise異步操做已經成功,這時,在異步操做成功以前註冊的回調都會執行,可是在Promise異步操做成功這以後調用的then註冊的回調就不再會執行了,這顯然不是咱們想要的。

加入狀態

恩,爲了解決上一節拋出的問題,咱們必須加入狀態機制,也就是你們熟知的pendingfulfilledrejected

Promises/A+規範中的2.1Promise States中明確規定了,pending能夠轉化爲fulfilledrejected而且只能轉化一次,也就是說若是pending轉化到fulfilled狀態,那麼就不能再轉化到rejected。而且fulfilledrejected狀態只能由pending轉化而來,二者之間不能互相轉換。一圖勝千言:

alt promise state

改進後的代碼是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
 
this.then = function (onFulfilled) {
if (state === 'pending') {
callbacks.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
 
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout( function () {
callbacks.forEach( function (callback) {
callback(value);
});
}, 0);
}
 
fn(resolve);
}

 

上述代碼的思路是這樣的:resolve執行時,會將狀態設置爲fulfilled,在此以後調用then添加的新回調,都會當即執行。

這裏沒有任何地方將state設爲rejected,爲了讓你們聚焦在覈心代碼上,這個問題後面會有一小節專門加入。

鏈式Promise

那麼這裏問題又來了,若是用戶再then函數裏面註冊的仍然是一個Promise,該如何解決?好比下面的例4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 例4
getUserId()
.then(getUserJobById)
.then( function (job) {
// 對job的處理
});
 
function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

這種場景相信用過promise的人都知道會有不少,那麼相似這種就是所謂的鏈式Promise

鏈式Promise是指在當前promise達到fulfilled狀態後,即開始進行下一個promise(後鄰promise)。那麼咱們如何銜接當前promise和後鄰promise呢?(這是這裏的難點)。

其實也不是辣麼難,只要在then方法裏面return一個promise就好啦。Promises/A+規範中的2.2.7就是這麼說噠(微笑臉)~

下面來看看這段暗藏玄機的then方法和resolve方法改造代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
 
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
 
function 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);
}
 
 
function 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);
}

咱們結合例4的代碼,分析下上面的代碼邏輯,爲了方便閱讀,我把例4的代碼貼在這裏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 例4
getUserId()
.then(getUserJobById)
.then( function (job) {
// 對job的處理
});
 
function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

 

  1. then方法中,建立並返回了新的Promise實例,這是串行Promise的基礎,而且支持鏈式調用。
  2. handle方法是promise內部的方法。then方法傳入的形參onFulfilled以及建立新Promise實例時傳入的resolve均被push到當前promisecallbacks隊列中,這是銜接當前promise和後鄰promise的關鍵所在(這裏必定要好好的分析下handle的做用)。
  3. getUserId生成的promise(簡稱getUserId promise)異步操做成功,執行其內部方法resolve,傳入的參數正是異步操做的結果id
  4. 調用handle方法處理callbacks隊列中的回調:getUserJobById方法,生成新的promisegetUserJobById promise
  5. 執行以前由getUserId promisethen方法生成的新promise(稱爲bridge promise)的resolve方法,傳入參數爲getUserJobById promise。這種狀況下,會將該resolve方法傳入getUserJobById promisethen方法中,並直接返回。
  6. getUserJobById promise異步操做成功時,執行其callbacks中的回調:getUserId bridge promise中的resolve方法
  7. 最後執行getUserId bridge promise的後鄰promisecallbacks中的回調。

更直白的能夠看下面的圖,一圖勝千言(都是根據本身的理解畫出來的,若有不對歡迎指正):

alt promise analysis

失敗處理

在異步操做失敗時,標記其狀態爲rejected,並執行註冊的失敗回調:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例5
function getUserId() {
return new Promise(function(resolve) {
//異步請求
http.get(url, function(error, results) {
if (error) {
reject(error);
}
resolve(results.id)
})
})
}
 
getUserId().then( function(id) {
//一些處理
}, function(error) {
console.log(error)
})

有了以前處理fulfilled狀態的經驗,支持錯誤處理變得很容易,只須要在註冊回調、處理狀態變動上都要加入新的邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
 
this.then = function (onFulfilled, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};
 
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
 
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
ret = cb(value);
callback.resolve(ret);
}
 
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve, reject);
return;
}
}
state = 'fulfilled';
value = newValue;
execute();
}
 
function reject(reason) {
state = 'rejected';
value = reason;
execute();
}
 
function execute() {
setTimeout( function () {
callbacks.forEach( function (callback) {
handle(callback);
});
}, 0);
}
 
fn(resolve, reject);
}

上述代碼增長了新的reject方法,供異步操做失敗時調用,同時抽出了resolvereject共用的部分,造成execute方法。

錯誤冒泡是上述代碼已經支持,且很是實用的一個特性。在handle中發現沒有指定異步操做失敗的回調時,會直接將bridge promise(then函數返回的promise,後同)設爲rejected狀態,如此達成執行後續失敗回調的效果。這有利於簡化串行Promise的失敗處理成本,由於一組異步操做每每會對應一個實際功能,失敗處理方法一般是一致的:

1
2
3
4
5
6
7
8
9
//例6
getUserId()
.then(getUserJobById)
.then( function (job) {
// 處理job
}, function (error) {
// getUserId或者getUerJobById時出現的錯誤
console.log(error);
});

異常處理

細心的同窗會想到:若是在執行成功回調、失敗回調時代碼出錯怎麼辦?對於這類異常,可使用try-catch捕獲錯誤,並將bridge promise設爲rejected狀態。handle方法改造以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
 
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
try {
ret = cb(value);
callback.resolve(ret);
} catch (e) {
callback.reject(e);
}
}

若是在異步操做中,屢次執行resolve或者reject會重複處理後續回調,能夠經過內置一個標誌位解決。

總結

剛開始看promise源碼的時候總不能很好的理解then和resolve函數的運行機理,可是若是你靜下心來,反過來根據執行promise時的邏輯來推演,就不難理解了。這裏必定要注意的點是:promise裏面的then函數僅僅是註冊了後續須要執行的代碼,真正的執行是在resolve方法裏面執行的,理清了這層,再來分析源碼會省力的多。

如今回顧下Promise的實現過程,其主要使用了設計模式中的觀察者模式:

  1. 經過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法註冊到被觀察者Promise對象中,同時返回一個新的Promise對象,以即可以鏈式調用。
  2. 被觀察者管理內部pending、fulfilled和rejected的狀態轉變,同時經過構造函數中傳遞的resolve和reject方法以主動觸發狀態轉變和通知觀察者。
相關文章
相關標籤/搜索