用Promise組織程序
一、Promise基本用法
很多文章介紹Promise給的例子是這樣的:
new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', location.href, true);
xhr.send(null);
xhr.addEventListener('readystatechange', function(e){
if(xhr.readyState === 4) {
if(xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr);
}
}
})
}).then(function(txt){
console.log();
}) 一定會有小朋友好奇,說尼瑪,這不是比回調還惡心?
這種寫法的確是能跑得起來啦……不過,按照Promise的設計初衷,我們編程需要使用的概念并非"Promise對象",而是promise函數,凡是以Promise作為返回值的函數,稱為promise函數(我暫且取了這個名字)。所以應該是這樣的:
function doSth() {
return new Promise(function(resolve, reject) {
//做點什么異步的事情
//結束的時候調用 resolve,比如:
setTimeout(function(){
resolve(); //這里才是真的返回
},1000)
})
} 如果你不喜歡這樣的寫法,還可以使用defer風格的promise
function doSth2() {
var defer = Promise.defer();
//做點什么異步的事情
//結束的時候調用 defer.resolve,比如:
setTimeout(function(){
defer.resolve(); //這里才是真的返回
},1000)
return defer.promise;
} 總之兩種是沒什么區別啦。
然后你就可以這么干:
doSth().then(doSth2).then(doSth);
這樣看起來就順眼多了吧。
其實說簡單點,promise最大的意義在于把嵌套的回調變成了鏈式調用(詳見第三節,順序執行),比如以下
//回調風格
loadImage(img1,function(){
loadImage(img2,function(){
loadImage(img3,function(){
});
});
});
//promise風格
Promise.resolve().then(function(){
return loadImage(img1);
}).then(function(){
return loadImage(img2);
}).then(function(){
return loadImage(img3);
}); 后者嵌套關系更少,在多數人眼中會更易于維護一些。
二、Promise風格的API
在去完cssconf回杭州的火車上,我順手把一些常見的JS和API寫成了promise方式:
function get(uri){
return http(uri, 'GET', null);
}
function post(uri,data){
if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) {
var params = [];
for(var p in data) {
if(data[p] instanceof Array) {
for(var i = 0; i < data[p].length; i++) {
params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i]));
}
} else {
params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
}
}
data = params.join('&');
}
return http(uri, 'POST', data || null, {
"Content-type":"application/x-www-form-urlencoded"
});
}
function http(uri,method,data,headers){
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method,uri,true);
if(headers) {
for(var p in headers) {
xhr.setRequestHeader(p, headers[p]);
}
}
xhr.addEventListener('readystatechange',function(e){
if(xhr.readyState === 4) {
if(String(xhr.status).match(/^2\d\d$/)) {
resolve(xhr.responseText);
} else {
reject(xhr);
}
}
});
xhr.send(data);
})
}
function wait(duration){
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
function waitFor(element,event,useCapture){
return new Promise(function(resolve, reject) {
element.addEventListener(event,function listener(event){
resolve(event)
this.removeEventListener(event, listener, useCapture);
},useCapture)
})
}
function loadImage(src) {
return new Promise(function(resolve, reject) {
var image = new Image;
image.addEventListener('load',function listener() {
resolve(image);
this.removeEventListener('load', listener, useCapture);
});
image.src = src;
image.addEventListener('error',reject);
})
}
function runScript(src) {
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = src;
script.addEventListener('load',resolve);
script.addEventListener('error',reject);
(document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script);
})
}
function domReady() {
return new Promise(function(resolve, reject) {
if(document.readyState === 'complete') {
resolve();
} else {
document.addEventListener('DOMContentLoaded',resolve);
}
})
} 看到了嗎,Promise風格API跟回調風格的API不同,它的參數跟同步的API是一致的,但是它的返回值是個Promise對象,要想得到真正的結果,需要在then的回調里面拿到。
值得一提的是,下面這樣的寫法:
waitFor(document.documentElement,'click').then(function(){
console.log('document clicked!')
}) 這樣的事件響應思路上是比較新穎的,不同于事件機制,它的事件處理代碼僅會執行一次。
通過這些函數的組合,我們可以更優雅地組織異步代碼,請繼續往下看。
三、使用Promise組織異步代碼
函數調用/并行
Promise.all跟then的配合,可以視為調用部分參數為Promise提供的函數。譬如,我們現在有一個接受三個參數的函數
function print(a, b, c) {
console.log(a + b + c);
} 現在我們調用print函數,其中a和b是需要異步獲取的:
var c = 10; print(geta(), getb(), 10); //這是同步的寫法 Promise.all([geta(), getb(), 10]).then(print); //這是 primise 的異步寫法
競爭
如果說Primise.all是promise對象之間的“與”關系,那么Promise.race就是promise對象之間的“或”關系。
比如,我要實現“點擊按鈕或者5秒鐘之后執行”
var btn = document.getElementsByTagName('button');
Promise.race(wait(5000), waitFor(btn, click)).then(function(){
console.log('run!')
}) 異常處理
異常處理一直是回調的難題,而promise提供了非常方便的catch方法:
在一次promise調用中,任何的環節發生reject,都可以在最終的catch中捕獲到
Promise.resolve().then(function(){
return loadImage(img1);
}).then(function(){
return loadImage(img2);
}).then(function(){
return loadImage(img3);
}).catch(function(err){
//錯誤處理
}) 這非常類似于JS的try catch功能。
復雜流程
接下來,我們來看比較復雜的情況。
promise有一種非常重要的特性:then的參數,理論上應該是一個promise函數,而如果你傳遞的是普通函數,那么默認會把它當做已經resolve了的promise函數。
這樣的特性讓我們非常容易把promise風格的函數跟已有代碼結合起來。
為了方便傳參數,我們編寫一個currying函數,這是函數式編程里面的基本特性,在這里跟promise非常搭,所以就實現一下:
function currying(){
var f = arguments[0];
var args = Array.prototype.slice.call(arguments,1);
return function(){
args.push.apply(args,arguments);
return f.apply(this,args);
}
} currying會給某個函數"固化"幾個參數,并且返回接受剩余參數的函數。比如之前的函數,可以這么玩:
var print2 = currying(print,11);
print2(2, 3); //得到 11 + 2 + 3 的結果,16
var wait1s = currying(wait,1000);
wait1s().then(function(){
console.log('after 1s!');
}) 有了currying,我們就可以愉快地來玩鏈式調用了,比如以下代碼:
Promise.race([
domReady().then(currying(wait,5000)),
waitFor(btn, click)])
.then(currying(runScript,'a.js'))
.then(function(){
console.log('loaded');
return Promise.resolve();
}); 表示“domReady發生5秒或者點擊按鈕后,加載腳本并執行,完畢后打印loaded”。
四、總結
promise作為一個新的API,是幾乎完全可polyfill的,這在JS發展中這是很少見的情況,它的API本身沒有什么特別的功能,但是它背后代表的編程思路是很有價值的。
來自:http://www.w3ctech.com/topic/721