Promise簡介
Promise 採用物件導向的方式封裝了回撥函式,可以將回撥金字塔改為平行的鏈式寫法,優雅的解決了回撥地獄,ES7帶來了非同步的終級解決方案async/await,可以用寫同步程式碼的方式編寫非同步程式碼,而Promise正是async/await的基石。
Promise 是一種設計模式,也是規範,歷史上曾經出現過Promise A/Promise A+/Promise B/Promise D四種規範,最終ES6選擇了Promise A+的方案,真理來之不易。
Promise表面上看起來比較簡單,你看,生成的Promise物件很純淨,只有then,catch,finally幾個方法,還有兩個隱藏的屬性:PromiseStatus和PromiseValue分別表示狀態和返回值
說到Promise狀態,眾所周知,只有pending,fulfilled,rejected 3種狀態,而且不可逆,不成功便成仁,從建立例項時的pending,呼叫成功resolve就變成fulfilled,失敗則變為rejected,整個模型非常簡單
但是深入瞭解,Promise還有許多的潛規則,要深入理解一項技術最好的辦法是造一個輪子。 Promise能夠一統江湖成為非同步的終極解決方案(配合async/await),它的價值絕對是不可估量的,值得你親手實現不止一遍。極簡版Promise
需求分析:
萬丈高樓平地起,一磚一瓦靠自己。我們先從最簡單的核心功能開始,第一步僅實現Promise的構造器函式和then方法 兩個功能
功能清單:
- Promise 構造器函式,傳入一個函式,該函式立即執行,並且有resolve和reject兩個引數,resolve被呼叫時Promise狀態變為fulfilled
- 實現then方法,傳入一個函式,該該數在Promise被fulfilled時執行
程式碼實現:
class PromiseA {
constructor(init) {
this.PromiseStatus = 'pending';
var resolve=(val)=>{
if(this.resolveCallback){
this.PromiseStatus="fulfilled"
this.resolveCallback(val);
}
}
if(init){
init(resolve,reject);
}
}
then(onFulfill,onReject) {
this.resolveCallback=onFulfill;
this.rejectCallback=onReject;
return this;
}
}
複製程式碼
就這麼簡單,花幾分鐘就可以寫好,寫個測試程式碼跑一下
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
})
複製程式碼
兩秒後輸出了:hello,from promise 1
完美執行,能夠轉得動的輪子就是好輪子!但是,好像還缺了點什麼?畢竟我們想做的是賓士車的輪子......
完整版Promise
需求分析
上一步我們做出了第一個能執行起來的Promise,但是還缺失一些必備功能,如下:
-
- 每次呼叫then方法應該返回一個新的Promise物件
-
- then方法支援鏈式呼叫,鏈式呼叫有兩種用法:
-
2.1 then註冊的onFulfill函式沒有返回值,則之後的then全部一起觸發 複製程式碼
-
2.2 then註冊的onFulfill函式返回了新的promise,則等這個新的promise fulfill之後,再觸發之後的then 複製程式碼
-
- reject函式,以及catch方法
實現思路:
精簡版的Promise 很容易實現和讀懂,但是要實現鏈式呼叫什麼的,就有點燒腦了,因為鏈式呼叫本身是連結串列的資料結構,又是高階函式傳來傳去,很容易繞暈,我是花了很久時間除錯修改,實現思路也是在除錯過程中才慢慢理清的,雖然只有幾行程式碼,但是用語言描述比較晦澀難懂,你非得單步除錯一下才能明白其中的奧妙。
- 首先,在then方法中返回一個新的promise不是什麼難事,new一下就可以了,但是then方法如果返回了promise,要用新的promise替代,問題是then中的promise已經先返回了,這是先有雞還是先有蛋的問題,時光不能倒流,那只有通過引用傳遞,改寫之前返回的promise了,其實也不用完全替換,只需要改寫resolve回撥就可以了。
- 對於then方法中不返回promise的情況,複製原promise的resolve回撥,就可以同時一起觸發多個then回撥
完整程式碼實現:
class PromiseA {
constructor(init) {
this.PromiseStatus = 'pending';
this.PromiseValue=null;
this.resolveCallback=null;
this.rejectCallback=null;
var resolve=(val)=>{
if(this.resolveCallback){
this.PromiseValue=val;
this.PromiseStatus="fulfilled"
var promiseNew=this.resolveCallback(val);
if(this.nextPromise){
let next=this.nextPromise;
if(promiseNew){ //then方法返回了新的promie,
promiseNew.resolveCallback=next.resolveCallback;
promiseNew.rejectCallback=next.rejectCallback;
}else if(next.resolveCallback!=this.resolveCallback){ //沒有返回新的promise,需要防重複呼叫
next.resolveCallback(val);
}
}
}
}
var reject=(val)=>{
if(this.rejectCallback){
this.PromiseStatus="rejected"
this.rejectCallback(val);
}
}
if(init){
init(resolve,reject);
}
}
then(onFulfill,onReject) {
this.resolveCallback=onFulfill;
this.rejectCallback=onReject;
var promise=new PromiseA();//建立一個新的promise例項
promise.resolveCallback=onFulfill;//新的promise例項的resolve函式預設指向當前promise,用來支援多次呼叫then
this.nextPromise=promise;//儲存一下新的promise引用,便於鏈式呼叫
return promise;
}
catch(onRejected){
return this.then(null, onRejected);
}
}
複製程式碼
寫個測試用例跑一下:
console.time("timer1");
console.time("timer2");
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
console.timeEnd("timer1");
}).then(function (msg){
console.log(msg)
console.timeEnd("timer2");
})
複製程式碼
執行後,兩個then回撥在2秒後同時觸發,說明第一種鏈式呼叫驗證成功
再測試一下第二種鏈式呼叫,測試程式碼如下:
console.time("timer1");
console.time("timer2");
new PromiseA(function (resolve){
setTimeout(function (){
resolve("hello,from promise 1");
},2000)
}).then(function (msg){
console.log(msg);
console.timeEnd("timer1");
return new PromiseA(function (resolve){
setTimeout(function (){
resolve("world,from promise 2")
},3000)
})
}).then(function (msg){
console.log(msg)
console.timeEnd("timer2");
})
複製程式碼
驗證成功,在2秒後觸發了第一個then回撥,並接收到了hello,from promise1的返回值,在5秒後觸發了第二個then回撥,並接收到了"world,from promise2"的返回值
實現Promise.all和Promise.race
Promise的例項功能已經完工了,翻翻看Promise構造器函式上還有兩個類方法all和race,其中Promise.all是一個非常有用的功能,可以併發執行多個非同步任務,全部成功後再執行resolve,無論是處理多個http並行請求,還是並行執行sql指令碼等平行計算任務,都十分方便。
有了上面的PromiseA類基礎設施,實現這個功能簡直不要太簡單。這次要用靜態方法,也叫類方法,就是在PromiseA構造器函式上定義的,es6 的class 中定義的方法預設是生成在例項的原型中的,加一個static關鍵字就可以變為靜態方法。
實現思路:
- 生成一個新的Promise。
- 遍歷傳入的promise陣列,依次呼叫每一個promise的then方法註冊回撥。
- 在then 回撥中把promise返回值push到一個結果陣列中,檢測結果陣列長度與promise陣列長度相等時表示所有promise都已經resolve了,再執行總的resolve。
Promise.race則更簡單,只有任意一個promise fulfilled就執行總的resolve。
程式碼如下:
static all(list){
return new PromiseA(function (resolve){
var results=[];
list.forEach((promise)=>{
promise.then((val)=>{
results.push(val);
if(results.length==list.length){
resolve(results);
}
})
})
})
}
static race(list){
return new PromiseA(function (resolve){
list.forEach((promise)=>{
promise.then((val)=>{
resolve(val);
})
})
});
}
複製程式碼
小結
盤點了一下,一共用了60多行程式碼,實現了Promise A+的絕大部分功能,除了一些語法糖和一些容錯處理沒有做,業務上能用到的所有功能都已經實現,全部程式碼都已通過測試可以直接執行,如果要做IE瀏覽器下的promise polyfill也是完全夠用的,為了增加可讀性,本文的程式碼用了es6 class和箭頭函式,稍做修改就可以改寫為相容es3標準的程式碼。
最後,推廣一下個人的開源專案,node.js web開發框架: webcontext