Promise這東西,只會用,沒有刻意去了解過。但有時不得不為它帶來的便利感到驚嘆。用的多了,對他的思想就會有一點了解,越來越覺得和消息訂閱模式有異曲同工之妙。
為什么要有promise呢。以讀文件為例:
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function(f){
}
如果想要操作文件內(nèi)容,就必須得在onload中進(jìn)行操作。很麻煩,項目中不可能永遠(yuǎn)寫這幾行代碼,所以封裝下
function readFile(file,fnc){
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function(f){
fnc();
}
}
通過傳入fnc函數(shù),讓FileReader對象在onload中調(diào)用。這種解決異步的方式叫回調(diào)。
好像promise目前沒有出場的必要。
直到有一天,老板讓你連續(xù)讀文件,前一個文件讀完才允許讀下一個,讀4個。
readFile(a,()=>{
readFile(b,()=>{
readFile(c,()=>{
readFile(d,()=>{
})
})
})
})
第一天寫完可能還好,記得調(diào)用順序,等過幾天再看到這坨估計就要抓狂了。真實代碼中更是會夾雜許多邏輯,加上幾個if else帶來的花括號,想要快速理清這個嵌套關(guān)系,說出執(zhí)行順序幾乎是不可能的。
所以,想要編寫可維護(hù)代碼,一種更順序的寫法來解決異步顯得十分重要。
最理想的方式當(dāng)然是這樣
readFile(a,funcA);
readFile(b,funcB);
readFile(c,funcC);
readFile(d,funcD);
但是他不能保證讀完A文件后才讀B。
突然,你靈機一動,想到了消息訂閱模式。不去直接執(zhí)行讀取文件的函數(shù),而是依次添加訂閱。這樣,順序性也就解決了。試試!
class Mypromise{
constructor(fn){
this._topics= []; //管理訂閱的事件
fn(this._resolve.bind(this)); //_resolve函數(shù)中用到了this,所以這里綁定下,保證能夠找到_topics對象
}
then(callback){ //添加訂閱
this._topics.push(callback);
return this; //為了持續(xù)添加訂閱
}
_resolve(val){ //發(fā)布消息
this._topics.forEach(callback=>{
callback(val);
})
}
}
使用then函數(shù)來添加訂閱,resolve函數(shù)發(fā)布消息。寫段測試代碼測試下,這里使用簡單的定時器來模擬異步操作:
function readFile(a){
return new Mypromise(resolve=>{
setTimeout(()=>{
console.log(a);
resolve(a);
},500)
})
}
readFile('a')
.then(()=>{
console.log('b')
})
.then(()=>{
console.log('c')
});
按順序打出了a b c。效果已經(jīng)出來了。這樣寫我們可以馬上看清誰先調(diào)用,誰后調(diào)用。并且解決了異步問題。
有人就有疑問了,如果readFile函數(shù)里全是同步代碼,你還沒有通過.then添加回調(diào)函數(shù),你就resolve了,那不就什么函數(shù)都沒有被調(diào)用。
記得setTimeout(function(){},0)這個經(jīng)典面試題嗎,它會將在0S后將函數(shù)推入事件隊列,當(dāng)前同步代碼執(zhí)行完后,才會開始執(zhí)行。所以只要用它把_resolve函數(shù)內(nèi)部實現(xiàn)包裹一下,就能解決這個問題。
_resolve(val){ //發(fā)布消息
setTimeout(()=>{
this._topics.forEach(callback=>{
callback(val);
})
},0)
}
這樣就能保證所有的.then都執(zhí)行完再resolve了。
Promise最好用的一點是每個then返回的都是一個新的Promise對象,而不是原來的promise實例,如下:
let p = Promise.resolve('1');
p.then(json=>{
console.log(1); //1
return 2
}).then(json=>{
console.log(json) //2
});
第二個會輸出2是因為第一個then返回了一個新的Promise對象。第二個then的回調(diào)加在了這個新的Promise對象中。所以我們的then函數(shù)不能return this,而是要return Mypromise。
then(callback){
this._topics.push(callback);
return new Mypromise(resolve=>{
resolve(callback());
})
}
但是這樣每次.then的時候立刻執(zhí)行了callback,顯然不符合要求。且沒有達(dá)成傳遞的要求。所以銜接前一個promise和后一個promise變得至關(guān)重要。
其實也簡單,調(diào)用then函數(shù)往_topics塞回調(diào)的時候不僅把callback塞進(jìn)去,也把新生成的promise對象的resolve也塞進(jìn)去。執(zhí)行resolve的時候不僅要執(zhí)行callback,也要執(zhí)行resolve,即觸發(fā)下一個then的回調(diào),修改后完整代碼如下:
class Mypromise{
constructor(fn){
this._topics= []; //管理訂閱的事件
fn(this._resolve.bind(this));
}
then(callback){ //添加訂閱
return new Mypromise(resolve=>{
this._topics.push({
callback:callback, //當(dāng)前then添加的回調(diào)函數(shù)
resolve:resolve //then新生成promise對象的resolve,用于觸發(fā)該promise回調(diào)
})
});
}
_resolve(val){ //發(fā)布消息
setTimeout(()=>{
this._topics.forEach(call=>{
var result = call.callback(val); //執(zhí)行當(dāng)前promise注冊的回調(diào)
call.resolve(result); //觸發(fā)新生成promise的回調(diào)
})
},0)
}
}
function readFile(a){
return new Mypromise(resolve=>{
setTimeout(()=>{
console.log(a);
resolve(1);
},500)
})
}
readFile('a')
.then(json=>{
console.log(json);
return 2;
})
.then(json=>{
console.log(json)
});
打印出了理想的a,1,2。一個極簡的promise就完成了。