前言
前一陣子記錄了promise的一些常規用法,這篇文章再深入一個層次,來分析分析promise的這種規則機制是如何實現的。ps:本文適合已經對promise的用法有所瞭解的人閱讀,如果對其用法還不是太瞭解,可以移步我的上一篇博文。
本文的promise原始碼是按照Promise/A+規範來編寫的(不想看英文版的移步Promise/A+規範中文翻譯)
引子
為了讓大家更容易理解,我們從一個場景開始講解,讓大家一步一步跟著思路思考,相信你一定會更容易看懂。
考慮下面一種獲取使用者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
怎麼實現呢?其實按照上面一句話,實現一個最基礎的雛形還是很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); } |
上述程式碼很簡單,大致的邏輯是這樣的:
- 呼叫
then
方法,將想要在Promise
非同步操作成功時執行的回撥放入callbacks
佇列,其實也就是註冊回撥函式,可以向觀察者模式方向思考; - 建立
Promise
例項時傳入的函式會被賦予一個函式型別的引數,即resolve
,它接收一個引數value,代表非同步操作返回的結果,當一步操作執行成功後,使用者會呼叫resolve
方法,這時候其實真正執行的操作是將callbacks
佇列中的回撥一一執行;
可以結合例1
中的程式碼來看,首先new Promise
時,傳給promise
的函式傳送非同步請求,接著呼叫promise
物件的then
屬性,註冊請求成功的回撥函式,然後當非同步請求傳送成功時,呼叫resolve(results.id)
方法, 該方法執行then
方法註冊的回撥陣列。
相信仔細的人應該可以看出來,then
方法應該能夠鏈式呼叫,但是上面的最基礎簡單的版本顯然無法支援鏈式呼叫。想讓then
方法支援鏈式呼叫,其實也是很簡單的:
1 2 3 4 |
this.then = function (onFulfilled) { callbacks.push(onFulfilled); return this; }; |
see?只要簡單一句話就可以實現類似下面的鏈式呼叫:
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
註冊的回撥就再也不會執行了,這顯然不是我們想要的。
加入狀態
恩,為了解決上一節丟擲的問題,我們必須加入狀態機制,也就是大家熟知的pending
、fulfilled
、rejected
。
Promises/A+
規範中的2.1Promise States
中明確規定了,pending
可以轉化為fulfilled
或rejected
並且只能轉化一次,也就是說如果pending
轉化到fulfilled
狀態,那麼就不能再轉化到rejected
。並且fulfilled
和rejected
狀態只能由pending
轉化而來,兩者之間不能互相轉換。一圖勝千言:
改進後的程式碼是這樣的:
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.onResolved) { 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); }); }); } |
then
方法中,建立並返回了新的Promise
例項,這是序列Promise
的基礎,並且支援鏈式呼叫。handle
方法是promise
內部的方法。then
方法傳入的形參onFulfilled
以及建立新Promise
例項時傳入的resolve
均被push
到當前promise
的callbacks
佇列中,這是銜接當前promise
和後鄰promise
的關鍵所在(這裡一定要好好的分析下handle的作用)。getUserId
生成的promise
(簡稱getUserId promise
)非同步操作成功,執行其內部方法resolve
,傳入的引數正是非同步操作的結果id
- 呼叫
handle
方法處理callbacks
佇列中的回撥:getUserJobById
方法,生成新的promise
(getUserJobById promise
) - 執行之前由
getUserId promise
的then
方法生成的新promise
(稱為bridge promise
)的resolve
方法,傳入引數為getUserJobById promise
。這種情況下,會將該resolve
方法傳入getUserJobById promise
的then
方法中,並直接返回。 - 在
getUserJobById promise
非同步操作成功時,執行其callbacks
中的回撥:getUserId bridge promise
中的resolve
方法 - 最後執行
getUserId bridge promise
的後鄰promise
的callbacks
中的回撥。
更直白的可以看下面的圖,一圖勝千言(都是根據自己的理解畫出來的,如有不對歡迎指正):
失敗處理
在非同步操作失敗時,標記其狀態為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
方法,供非同步操作失敗時呼叫,同時抽出了resolve
和reject
共用的部分,形成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的實現過程,其主要使用了設計模式中的觀察者模式:
- 通過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法註冊到被觀察者Promise物件中,同時返回一個新的Promise物件,以便可以鏈式呼叫。
- 被觀察者管理內部pending、fulfilled和rejected的狀態轉變,同時通過建構函式中傳遞的resolve和reject方法以主動觸發狀態轉變和通知觀察者。