前一陣子記錄了promise
的一些常規用法,這篇文章再深刻一個層次,來分析分析promise
的這種規則機制是如何實現的。ps:本文適合已經對promise的用法有所瞭解的人閱讀,若是對其用法還不是太瞭解,能夠移步個人上一篇博文。設計模式
本文的promise
源碼是按照Promise/A+
規範來編寫的(不想看英文版的移步Promise/A+
規範中文翻譯)數組
爲了讓你們更容易理解,咱們從一個場景開始講解,讓你們一步一步跟着思路思考,相信你必定會更容易看懂。promise
考慮下面一種獲取用戶id的請求處理異步
//例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的。this
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); }
上述代碼很簡單,大體的邏輯是這樣的:url
then
方法,將想要在Promise
異步操做成功時執行的回調放入callbacks
隊列,其實也就是註冊回調函數,能夠向觀察者模式方向思考;Promise
實例時傳入的函數會被賦予一個函數類型的參數,即resolve
,它接收一個參數value
,表明異步操做返回的結果,當一步操做執行成功後,用戶會調用resolve
方法,這時候其實真正執行的操做是將callbacks
隊列中的回調一一執行;能夠結合例1中的代碼來看,首先new Promise
時,傳給promise
的函數發送異步請求,接着調用promise
對象的then屬性,註冊請求成功的回調函數,而後當異步請求發送成功時,調用resolve(results.id)
方法, 該方法執行then
方法註冊的回調數組。spa
相信仔細的人應該能夠看出來,then
方法應該可以鏈式調用,可是上面的最基礎簡單的版本顯然沒法支持鏈式調用。想讓then
方法支持鏈式調用,其實也是很簡單的:prototype
this.then = function (onFulfilled) { callbacks.push(onFulfilled); return this; };
see?只要簡單一句話就能夠實現相似下面的鏈式調用:翻譯
// 例2 getUserId().then(function (id) { // 一些處理 }).then(function (id) { // 一些處理 });
細心的同窗應該發現,上述代碼可能還存在一個問題:若是在then
方法註冊回調以前,resolve
函數就執行了,怎麼辦?好比promise
內部的函數是同步函數:
// 例3 function getUserId() { return new Promise(function (resolve) { resolve(9876); }); } getUserId().then(function (id) { // 一些處理 });
這顯然是不容許的,Promises/A+
規範明確要求回調須要經過異步方式執行,用以保證一致可靠的執行順序。所以咱們要加入一些處理,保證在resolve
執行以前,then
方法已經註冊完全部的回調。咱們能夠這樣改造下resolve
函數:
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
轉化而來,二者之間不能互相轉換。一圖勝千言:
改進後的代碼是這樣的:
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
,爲了讓你們聚焦在覈心代碼上,這個問題後面會有一小節專門加入。
那麼這裏問題又來了,若是用戶再then函數裏面註冊的仍然是一個Promise
,該如何解決?好比下面的例4:
// 例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
方法改造代碼:
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的代碼貼在這裏:
// 例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中的
resolve`方法getUserId bridge promise
的後鄰promise
的callbacks
中的回調。更直白的能夠看下面的圖,一圖勝千言(都是根據本身的理解畫出來的,若有不對歡迎指正):
在異步操做失敗時,標記其狀態爲rejected
,並執行註冊的失敗回調:
//例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
狀態的經驗,支持錯誤處理變得很容易,只須要在註冊回調、處理狀態變動上都要加入新的邏輯:
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
的失敗處理成本,由於一組異步操做每每會對應一個實際功能,失敗處理方法一般是一致的:
//例6 getUserId() .then(getUserJobById) .then(function (job) { // 處理job }, function (error) { // getUserId或者getUerJobById時出現的錯誤 console.log(error); });
細心的同窗會想到:若是在執行成功回調、失敗回調時代碼出錯怎麼辦?對於這類異常,可使用try-catch
捕獲錯誤,並將bridge promise
設爲rejected
狀態。handle
方法改造以下:
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方法以主動觸發狀態轉變和通知觀察者。