本文來自網易雲社群。
前一陣子記錄了promise的一些正常用法,這篇文章再深入一個層次,來分析分析promise的這種規則機制是如何實作的。ps:本文适合已經對promise的用法有所了解的人閱讀,如果對其用法還不是太了解,可以移步我的上一篇博文。
本文的promise源碼是按照Promise/A+規範來編寫的(不想看英文版的移步Promise/A+規範中文翻譯)
引子
為了讓大家更容易了解,我們從一個場景開始講解,讓大家一步一步跟着思路思考,相信你一定會更容易看懂。
考慮下面一種擷取使用者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的。
極簡promise雛形
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
,它接收一個參數value,代表異步操作傳回的結果,當一步操作執行成功後,使用者會調用resolve
方法,這時候其實真正執行的操作是将resolve
隊列中的回調一一執行;callbacks
可以結合
例1
中的代碼來看,首先
new Promise
時,傳給
promise
的函數發送異步請求,接着調用
promise
對象的
then
屬性,注冊請求成功的回調函數,然後當異步請求發送成功時,調用
resolve(results.id)
方法, 該方法執行
then
方法注冊的回調數組。
相信仔細的人應該可以看出來,
then
方法應該能夠鍊式調用,但是上面的最基礎簡單的版本顯然無法支援鍊式調用。想讓
then
方法支援鍊式調用,其實也是很簡單的:
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.1
Promise States
中明确規定了,
pending
可以轉化為
fulfilled
或
rejected
并且隻能轉化一次,也就是說如果
pending
轉化到
fulfilled
狀态,那麼就不能再轉化到
rejected
。并且
fulfilled
和
rejected
狀态隻能由
pending
轉化而來,兩者之間不能互相轉換。一圖勝千言:
<img src="https://pic4.zhimg.com/v2-a90a6c703c051370aee8311ec00ef4a5_b.jpg" data-caption="" data-size="normal" data-rawwidth="353" data-rawheight="260" class="content_image" width="353">
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiInBnauQGafVTY0YWZwAzYlFTMzgTZlFGM3MTM1AzYzAzNjZTYwkTYtIjdvwFM48CXt92YucWbphmeuQzYpB3Lc9CX6MHc0RHaiojIsJye.jpg)
改進後的代碼是這樣的:
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
:
// 例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
的關鍵所在(這裡一定要好好的分析下handle的作用)。promise
-
生成的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
更直白的可以看下面的圖,一圖勝千言(都是根據自己的了解畫出來的,如有不對歡迎指正):
<img src="https://pic3.zhimg.com/v2-73684d65d37e4179096f60b004785263_b.jpg" data-caption="" data-size="normal" data-rawwidth="2137" data-rawheight="1262" class="origin_image zh-lightbox-thumb" width="2137" data-original="https://pic3.zhimg.com/v2-73684d65d37e4179096f60b004785263_r.jpg">
失敗處理
在異步操作失敗時,标記其狀态為
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方法以主動觸發狀态轉變和通知觀察者。
參考文獻
深入了解 Promise
JavaScript Promises ... In Wicked Detail
本文來自網易雲社群,經作者顧靜授權釋出。