Promise 簡介
(該部分轉載自 blog.csdn.net/u010576399/… ) Promise物件是CommonJS工作組提出的一種規範,目的是為非同步操作提供統一介面.
那麼,什麼是Promises?
首先,它是一個物件,也就是說與其他JavaScript物件的用法,沒有什麼兩樣;其次,它起到代理作用(proxy),充當非同步操作與回撥函式之間的中介。它使得非同步操作具備同步操作的介面,使得程式具備正常的同步執行的流程,回撥函式不必再一層層巢狀。
簡單說,它的思想是,每一個非同步任務立刻返回一個Promise物件,由於是立刻返回,所以可以採用同步操作的流程。這個Promises物件有一個then方法,允許指定回撥函式,在非同步任務完成後呼叫。
比如,非同步操作f1返回一個Promise物件,它的回撥函式f2寫法如下:
(new Promise(f1)).then(f2);
複製程式碼
這種寫法對於多層巢狀的回撥函式尤其方便。
// 傳統寫法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promises的寫法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
複製程式碼
從上面程式碼可以看到,採用Promises介面以後,程式流程變得非常清楚,十分易讀。
注意,為了便於理解,上面程式碼的Promise物件的生成格式,做了簡化。
總的來說,傳統的回撥函式寫法使得程式碼混成一團,變得橫向發展而不是向下發展。Promises規範就是為了解決這個問題而提出的,目標是使用正常的程式流程(同步),來處理非同步操作。它先返回一個Promise物件,後面的操作以同步的方式,寄存在這個物件上面。等到非同步操作有了結果,再執行前期寄放在它上面的其他操作。
Promises原本只是社群提出的一個構想,一些外部函式庫率先實現了這個功能。ECMAScript 6將其寫入語言標準,因此目前JavaScript語言原生支援Promise物件。
手動實現一個滿足promises-aplus-tests的Promise
promisesaplus.com/ 是一個介紹promise如何實現的一個網站. 根據該網站提供的介紹資訊, 我們可以嘗試自己寫一個promise並使用提供的promises-aplus-tests工具對所寫的promise進行測試.
Step1
首先 實現promise最基本的功能: 即在promise建立以後執行執行器中的程式碼,在then的時候相應的函式得以執行. 需要注意的是,當執行器中有錯誤丟擲的時候,應該捕獲錯誤並直接執行reject
//實現功能: 當執行器中呼叫resolve(),則then中只執行onFulfiled方法,執行器中呼叫reject(),則then中只執行onRejected方法, 當兩個方法都有的時候,以先執行的方法為準,後執行的方法對then不產生影響.
function Promise (executor) { //執行器
let self = this;
self.status = 'pending'; //引入狀態,對兩個方法都有的情況進行區分
self.value = undefined; //預設值
self.reason = undefined;
function resolve(data_value) {
if(self.status === 'pending') {
self.status = 'resolved';
self.value = data_value;
}
}
function reject(data_reason) {
if(self.status === 'pending') {
self.status = 'rejected';
self.reason = data_reason;
}
}
//如果executor是同步程式碼 進行try catch獲取其中的異常 如果有異常 把異常傳到reject
try {
executor(resolve, reject);
} catch (e) {
reject(e); //呼叫reject並把捕獲的error作為引數傳給reject
}
}
Promise.prototype.then = function (onFulfiled, onRejected) {
let self = this;
if(self.status === 'resolved') {
onFulfiled(self.value);
}
if(self.status === 'rejected') {
onRejected(self.value);
}
}
複製程式碼
step2
那麼問題來了, 如果執行器中有非同步程式碼, 上面的實現方法就會出問題,因為在執行
executor(resolve, reject);
複製程式碼
的時候,非同步程式碼不會立即執行, then中沒有判斷非同步程式碼是否已經執行的機制. 我們的解決方案是: 在then中判定狀態是否為pending,如果狀態為pending(此時為建構函式new Promise()執行期間), 則把then中的執行函式onFulfiled, onRejected先存入佇列(用array實現),當狀態改變後執行這些方法.
function Promise (executor) { //執行器
let self = this;
self.status = 'pending';
self.value = undefined; //預設值
self.reason = undefined;
self.onResolvedCallbacks = []; //存放then成功的回撥 陣列
self.onRejectedCallbacks = []; //存放then失敗的回撥 陣列
function resolve(data_value) {
if(self.status === 'pending') {
self.status = 'resolved';
self.value = data_value;
self.onResolvedCallbacks.forEach(function(fn) { //呼叫resolve的時候執行儲存在onRejectedCallbacks的函式
fn();
})
}
}
function reject(data_reason) {
if(self.status === 'pending') {
self.status = 'rejected';
self.reason = data_reason;
self.onRejectedCallbacks.forEach(function(fn) { //呼叫resolve的時候執行儲存在onRejectedCallbacks的函式
fn();
})
}
}
try {
executor(resolve, reject); //當executor中有非同步程式碼時 這部分不會立即執行(但是前面的部分在new的時候還是會執行)
} catch (e) {
reject(e);
}
}
Promise.prototype.then = function (onFulfiled, onRejected) {
let self = this;
if(self.status === 'resolved') {
onFulfiled(self.value);
}
if(self.status === 'rejected') {
onRejected(self.reason);
}
//當呼叫then時可能沒成功 也沒失敗
if(self.status === 'pending') { //此時沒有resolve也沒有reject
self.onResolvedCallbacks.push(function(){ //用陣列是為了保證在非同步時有多次promise.then的情況
onFulfiled(self.value);
});
self.onRejectedCallbacks.push(function(){
onRejected(self.reason);
});
}
}
複製程式碼
step3
到現在為止,我們僅僅完成了promise最基本的功能, 但是promise中一個重要的功能: then的鏈式呼叫尚未實現. 解決的思路類似於jquery的鏈式呼叫, 區別則是:jquery是返回this 這裡則是返回一個新的promise. 我們在then中新建一個變數promise2. 以
self.status === 'resolved'
複製程式碼
情況為例 將之前的程式碼修改為:
if(self.status === 'resolved') {
promise2 = new Promise(function(resolve, reject){ //將前一次then中的執行函式放入新的Promise的executor得到promise2作為下次then的返回值
onFulfiled(self.value); //注意這時返回的promise的執行器執行的是onFulfiled函式 而不是resolve或者reject.
})
}
複製程式碼
但是以上程式碼中, promise2在建立的時候, 並沒有設定resolve/reject的規則,因此只能算作半成品. 根據規定, 如果onFulfile/onRejected有返回值, 則將返回值作為resolve/reject的引數傳入,這樣,下一次.then就有狀態,不再是無根之木. 根據返回值的不同, 又將返回值分別以普通值和promise進行分別處理. 為此我們引入了一個統一的處理方法,以resolve()為例, 對應的處理方法我們取名為resolvePromise(promise2, x, resolve, reject) 其中x為onFulfiled的返回值. resolvePromise程式碼如下所示:
function resolvePromise(promise2, x, resolve, reject) {
//有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫
if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
return reject(new TypeError('迴圈引用'));
}
//看x是不是一個promise promise應該是一個物件
if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
//可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, function(y) {
//成功
resolvePromise(promise2, y, resolve, reject)
}, function (err) {
//失敗
reject(err);
})
} else {
resolve(x) //如果then不是函式 則把x作為返回值.
}
} catch (e) {
reject(e)
}
} else { //普通值
return resolve(x)
}
}
複製程式碼
相應的
self.status === 'resolved'
複製程式碼
時的程式碼則為:
if(self.status === 'resolved') {
promise2 = new Promise(function(resolve, reject){
let x = onFulfiled(self.value);
resolvePromise(promise2, x, resolve, reject);
})
}
複製程式碼
為例保證程式碼的通用性,考慮到有可能其他人的promise寫的不正確,可能會既呼叫成功又呼叫失敗的情況, 我們應當在程式碼中對返回的promise進行判斷: 如果兩個都呼叫 先呼叫誰 另一個忽略掉. 為此引入一個變數called.
function resolvePromise(promise2, x, resolve, reject) {
//有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫
if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
return reject(new TypeError('迴圈引用'));
}
//看x是不是一個promise promise應該是一個物件
let called; //表示是否呼叫過成功或者失敗
if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
//可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
try {
let then = x.then;
if (typeof then === 'function') {
//成功
then.call(x, function(y) {
if (called) return
called = true;
resolvePromise(promise2, y, resolve, reject)
}, function(err) {
if (called) return
called = true;
reject(err);
})
} else {
resolve(x) //如果then不是函式 則把x作為返回值.
}
} catch (e) {
if (called) return
called = true;
reject(e)
}
} else { //普通值
return resolve(x)
}
}
複製程式碼
Step4
到目前為止, promise的大框架基本完成. 接下來需要解決的兩個小問題是值的穿透以及onFulfiled/onRejected的非同步執行問題. 值的穿透的含義就是, 當then中使用沒有任何方法, onFulfiled()中的data自動作為返回值. 實現起來也很簡單, 在then的定義的最開始部分做一個判斷:
Promise.prototype.then = function (onFulfiled, onRejected) {
//成功和失敗預設不傳, 給一個預設函式 可以實現值的穿透
onFulfiled = typeof onFulfiled === 'function'? onFulfiled:function(value) {
return value;
}
onRejected = typeof onRejected === 'function'? onRejected:function(err) {
throw err; //在值的穿透的情況下 應該走下一個then的onRejected而不是onFulfiled 保證邏輯的一致性
}
.....
}
複製程式碼
對於onFulfild/onRejected非同步執行的問題, 則是在
if(self.status === 'resolved') {
promise2 = new Promise(function(resolve, reject){
let x = onFulfiled(self.value);
resolvePromise(promise2, x, resolve, reject)
...
}
}
複製程式碼
程式碼塊使用setTimeout. 這時帶來的副作用就是之前在promise建構函式中的程式碼塊
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
複製程式碼
不能捕獲到setTimeout中的非同步函式, 因此需要在setTimeout中也需要包一層try/catch:
if(self.status === 'resolved') {
promise2 = new Promise(function(resolve, reject){
setTimeout(function(){ //用setTimeOut實現非同步
try {
let x = onFulfiled(self.value); //x可能是普通值 也可能是一個promise, 還可能是別人的promise
resolvePromise(promise2, x, resolve, reject) //寫一個方法統一處理
} catch (e) {
reject(e);
}
})
})
}
複製程式碼
step5
最終程式碼如下所示:
function Promise (executor) {
let self = this;
self.status = 'pending';
self.value = undefined;
self.reason = undefined;
self.onResolvedCallbacks = [];
self.onRejectedCallbacks = [];
function resolve(data_value) {
if(self.status === 'pending') {
self.status = 'resolved';
self.value = data_value;
self.onResolvedCallbacks.forEach(function(fn) {
fn();
})
}
}
function reject(data_reason) {
if(self.status === 'pending') {
self.status = 'rejected';
self.reason = data_reason;
self.onRejectedCallbacks.forEach(function(fn) {
fn();
})
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolvePromise(promise2, x, resolve, reject) {
//有可能這裡返回的x是別人的promise 要儘可能允許其他人亂寫
if(promise2 === x) {//這裡應該報一個迴圈引用的型別錯誤
return reject(new TypeError('迴圈引用'));
}
//看x是不是一個promise promise應該是一個物件
let called; //表示是否呼叫過成功或者失敗
if (x!== null && (typeof x ==='object' ||typeof x === 'function')) {
//可能是promise 看這個物件中是否有then 如果有 姑且作為promise 用try catch防止報錯
try {
let then = x.then;
if (typeof then === 'function') {
//成功
then.call(x, function(y) {
if (called) return //避免別人寫的promise中既走resolve又走reject的情況
called = true;
resolvePromise(promise2, y, resolve, reject)
}, function(err) {
if (called) return
called = true;
reject(err);
})
} else {
resolve(x) //如果then不是函式 則把x作為返回值.
}
} catch (e) {
if (called) return
called = true;
reject(e)
}
} else { //普通值
return resolve(x)
}
}
Promise.prototype.then = function (onFulfiled, onRejected) {
//成功和失敗預設不傳給一個函式
onFulfiled = typeof onFulfiled === 'function'? onFulfiled:function(value) {
return value;
}
onRejected = typeof onRejected === 'function'? onRejected:function(err) {
throw err;
}
let self = this;
let promise2; //新增: 返回的promise
if(self.status === 'resolved') {
promise2 = new Promise(function(resolve, reject){
setTimeout(function(){ //用setTimeOut實現非同步
try {
let x = onFulfiled(self.value); //x可能是普通值 也可能是一個promise, 還可能是別人的promise
resolvePromise(promise2, x, resolve, reject) //寫一個方法統一處理
} catch (e) {
reject(e);
}
})
})
}
if(self.status === 'rejected') {
promise2 = new Promise(function(resolve, reject){
setTimeout (function() {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
})
}
if(self.status === 'pending') {
promise2 = new Promise (function(resolve, reject) {
self.onResolvedCallbacks.push(function(){
setTimeout(function(){
try {
let x = onFulfiled(self.value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
});
self.onRejectedCallbacks.push(function(){
setTimeout(function(){
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
})
});
})
}
return promise2;
}
複製程式碼
使用promises-aplus-tests對該方法進行測試, 最終測試得到通過. 如圖所示:
當然, 該promise相對於原生的promise還有一些不同,比如沒有實現catch功能,沒有靜態方法等. 這些部分我們將下次進行詳細討論.