用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