實現一個自己的promise
阿新 • • 發佈:2019-01-26
這是小弟的一篇開篇小作,如有不當之處,請各位道友批評指正。本文將探討Promise的實現。
一、ES6中的Promise
1、簡介
據說js很早就實現了Promise,我是不知道的,我第一次接觸Promise就是在ES6中。Promise就是規定在未來達到某個狀態時應該採取某種行動,而這種未來的狀態是不確定的。阮一峰說Promise物件用來傳遞非同步訊息,代表了某個未來才會知道結果的事件,併為這個事件提供統一的API,供進一步處理。 Promise和事件有本質區別:Promise是用一次就扔,而事件可以被多次觸發;Promise必定會產生一個訊號,要麼resolved(fulfilled),要麼rejected,而事件可能不被觸發。
2、基本用法
var p = new Promise(function(resolve){
setTimeout(function(){
resolve(111);
},1000)
});
p.then(function(value){
console.log("承諾解決了,拿到的資料為:"+value);
});
上面建立了一個promise,1秒後解決,然後用then方法添加了狀態改變的回撥函式。then方法中可以指定兩個函式,第一個位承諾是變成resolved狀態的回撥函式,第二個是承諾變為rejected狀態的回撥函式(可省略)。也可以在catch方法中指定承諾變為rejected的函式。如下:
p.catch(function(error){
console.log("rejected callback");
})
3、Promise.all()的用法
它接受一個promise物件陣列為引數,當陣列中的每一個promise都變成resolved狀態時,整個才算為resolved,陣列中promise的返回值將組成一個數組傳遞給Promise.all()返回的promise的回撥函式。陣列中的只要有一個為rejected整個為rejected;
var p1 = new Promise(function(resolve){
resolve("p1 resolved" );
});
var p2 = new Promise(function(resolve){
resolve('p2 resolved');
});
var p3 = new Promise(function(resolve){
resolve("p3 resolved");
});
var p = Promise.all([p1,p2,p3]);//這裡得到了一個新的promise
p.then(function(value){
console.log("p resolved, 得到的引數為:"+value);
})
.catch(function(error){
console.log("p rejected,"+error);
});
//output:p resolved, 得到的引數為:p1 resolved,p2 resolved,p3 resolved
將p2改為rejected
var p1 = new Promise(function(resolve){
resolve("p1 resolved");
});
var p2 = new Promise(function(resolve,reject){
reject('p2 rejected');
});
var p3 = new Promise(function(resolve){
resolve("p3 resolved");
});
p = Promise.all([p1,p2,p3]);
p.then(function(value){
console.log("p resolved, 得到的引數為:"+value);
})
.catch(function(error){
console.log("p rejected,"+error);
});
//output:p rejected,p2 rejected
4、Promise.race()的用法
它和Promise.all()的引數相同,它返回的promise的狀態隨著promise陣列中第一個狀態發生改變的promise的狀態而改變。
var p1 = new Promise(function(resolve){
resolve("p1 resolved");
});
var p2 = new Promise(function(resolve,reject){
reject('p2 rejected');
});
var p3 = new Promise(function(resolve){
resolve("p3 resolved");
});
p = Promise.race([p1,p2,p3]);
p.then(function(value){
console.log("p resolved, 得到的引數為:"+value);
})
.catch(function(error){
console.log("p rejected,"+error);
});
//output:p resolved, 得到的引數為:p1 resolved
我們發現雖然p2是rejected,但是p是resolved,因為第一個狀態變化的是p1,而p1是resolved。如果p1是rejected,那麼p一定是rejected。
上面就是ES6原生支援的Promise,那麼我們該如何實現一個類似的自己的Promise呢?都原生支援了為什麼還要自己實現呢?第一它是一種樂趣。第二它可以幫助我們更好的認識釋出訂閱模式。下面開始正式戰鬥!(有木有很激動,終於要開始了)。
二、實現自己的Promise
1、基礎實現
function Promise(fn) {
var value = null,
deferreds = [];
this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
};
function resolve(value) {
deferreds.forEach(function (deferred) {
deferred(value);
});
}
fn(resolve);
}
這個建構函式傳進來一個function,聲明瞭倆個內部變數,value傳到其內部方法resolve,defferreds儲存回撥函式,每次呼叫例項的then方法會讓then中的函式入隊,其內部方法resolve接受一個引數,它遍歷了defferreds佇列,並執行其中的方法。這個內部方法被傳遞給了建構函式的引數fn。結合ES6中Promise的基本用法應該不難理解這段程式碼。其then方法相當於是一個訂閱過程,resolve方法相當於一個釋出過程。
var mypromise = new Promise(function(resolve){
setTimeout(function(){
resolve("qqq");
},1000)
});
mypromise.then(function(value){
console.log(value); //qqq
})
2、問題修復
上述Promise的問題是,如果傳進去的不是一個非同步函式,那麼resolve方法會先執行,此時還沒有呼叫then,也就是說還沒有人訂閱,defferreds佇列還是空的,不合預期。改進如下:
function Promise(fn) {
var value = null,
deferreds = [];
this.then = function (onFulfilled) {
deferreds.push(onFulfilled);
};
function resolve(value) {
//這裡將resolve放到了棧底,所以then會先執行(如果有then的話)
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}
fn(resolve);
}
3、考慮一種情況:如果回撥函式註冊的很晚會怎麼樣
var mypromise = new Promise(function(resolve){
resolve("qqq");
});
setTimeout(function(){
mypromise.then(function(value){
console.log(value);
})
},1000);
結果是啥也沒做,為什麼呢?因為當我們註冊回撥的時候resolve已經執行了。那可咋整呢?解決方法就是記住Promise的狀態。請看程式碼:
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled) {
if (state === 'pending') {
deferreds.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}
fn(resolve);
}
就是這麼簡單,我們為Promise增加了一個內部變數state儲存其狀態,初始為pending(等待的意思),當resolve時,改變其狀態為fulfilled,調then的時候做個判斷,如果是pending說明resolve方法還沒執行,那麼我們將回調函式加到佇列等待resolve即可,如果是fulfilled,說明resolve已經執行,那麼我們直接執行新加入的回撥函式。至於那個return this,如果你用過jquery就知道了。
4、Promise鏈
我們考慮這樣的情況
var mypromise = new Promise(function(resolve){
resolve("first promise");
});
mypromise.then(function(value){
console.log(value);
return new Promise(function(resolve){
resolve("second promise");
})
})
.then(function(v){
console.log(v);
})
//結果:first promise
// first promise
//為啥是兩個first promise,這個是必然的,好好想想。第一個then返回的是前一個promise,而不是新建立的promise。
下面做一件有難度的是,我們來實現promise鏈,我看了幾個小時才搞明白。不要怕,打起精神來,我們開始。
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}
var ret = deferred.onFulfilled(value);
deferred.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 () {
deferreds.forEach(function (deferred) {
handle(deferred);
});
}, 0);
}
then方法中返回了一個新的promise例項,在新例項中調了一個內部方法,傳進去一個回撥函式和一個resolve方法構成的物件;內部方法判斷如果當前promise沒有調resolve的話,將傳入的物件入隊,否則的話直接調傳入的回撥函式,將回調函的返回值交給resolove方法。想一下,我們的回撥函式會返回什麼值,可能是一個promise物件也可能是字串或者undefined。如果說返回了一個物件,證明使用者又返回了一個promise,此時我們將使用者返回的promise的then拿到,然後調這個then,如果不是個物件或者函式,證明使用者沒有返回新的promise,此時直接resolve。
5、拒絕狀態
前面講了resolve,reject就是手到禽來了。
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};
function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}
var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? deferred.resolve : deferred.reject;
cb(value);
return;
}
ret = cb(value);
deferred.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;
finale();
}
function reject(reason) {
state = 'rejected';
value = reason;
finale();
}
function finale() {
setTimeout(function () {
deferreds.forEach(function (deferred) {
handle(deferred);
});
}, 0);
}
fn(resolve, reject);
}
6、處理resolve和reject的回撥函式異常
//改造了內部函式handle
function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}
var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? deferred.resolve : deferred.reject;
cb(value);
return;
}
try {
ret = cb(value);
deferred.resolve(ret);
} catch (e) {
deferred.reject(e);
}
}
真是無語,我已經寫完了,Promise.all()和Promise.race()的實現和總結都寫好了。已為新增參考文獻是那個引用,按了快捷鍵Ctrl+Q,結果退出了瀏覽器,氣死了。不寫了,大家可以參考文章最後的參考文獻,不過那個race方法的實現可能還有點問題,返回的promise的狀態不是跟第一個陣列中狀態發生改變的promise的狀態一致,而是最後一個。
三、總結
第一次寫部落格,突然那些寫了那麼多優秀文章的作者表示由衷佩服。這個Promise的實現確實是非常巧妙的,如果真的要我獨自實現,恐怕還需要一些時日。不管怎樣,終於完成了我的開篇之作。寫作過程中對Promise的實現有了進一步認識。最後希望自己能夠堅持寫部落格,在寫作中學習。