一轉眼,這2015年上半年就過去了,差不多一個月沒有寫博客了,"罪過罪過"啊~~。進入了七月份,也就意味着我們上半年苦逼的單身生活結束了,從此刻起,我們要打起十二分的精神,開始下半年的單身生活。大家一起加油~~
一直以來,JavaScript處理異步都是以callback的方式,在前端開發領域callback機制幾乎深入人心。在設計API的時候,不管是瀏覽器廠商還是SDK開發商亦或是各種類庫的作者,基本上都已經遵循着callback的套路。近幾年隨着JavaScript開發模式的逐漸成熟,CommonJS規范順勢而生,其中就包括提出了Promise規范,Promise完全改變了js異步編程的寫法,讓異步編程變得十分的易於理解。今天我們就來了解一下Promise~~
1、什么是promise?
Promise可能大家都不陌生,因為Promise規范已經出來好一段時間了,同時Promise也已經納入了ES6,而且高版本的chrome、firefox瀏覽器都已經原生實現了Promise,只不過和現如今流行的類Promise類庫相比少些API。
所謂Promise,字面上可以理解為“承諾”,就是說A調用B,B返回一個“承諾”給A,然后A就可以在寫計划的時候這么寫:當B返回結果給我的時候,A執行方案S1,反之如果B因為什么原因沒有給到A想要的結果,那么A執行應急方案S2,這樣一來,所有的潛在風險都在A的可控范圍之內了。
Promise規范如下:
- 一個promise可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
- 一個promise的狀態只可能從“等待”轉到“完成”態或者“拒絕”態,不能逆向轉換,同時“完成”態和“拒絕”態不能相互轉換
- promise必須實現
then
方法(可以說,then就是promise的核心),而且then必須返回一個promise,同一個promise的then可以調用多次,並且回調的執行順序跟它們被定義時的順序一致 - then方法接受兩個參數,第一個參數是成功時的回調,在promise由“等待”態轉換到“完成”態時調用,另一個是失敗時的回調,在promise由“等待”態轉換到“拒絕”態時調用。同時,then可以接受另一個promise傳入,也接受一個“類then”的對象或方法,即thenable對象。
2.promise原理分析
可以看到promise的規范並不是很多,下面我們一邊分析promise一邊自己寫一個promise的實現。Promise實現的大致思路如下:
構造函數Promise接受一個函數resolver
,可以理解為傳入一個異步任務,resolver接受兩個參數,一個是成功時的回調,一個是失敗時的回調,這兩參數和通過then傳入的參數是對等的。
其次是then的實現,由於Promise要求then必須返回一個promise,所以在then調用的時候會新生成一個promise,掛在當前promise的_next上,同一個promise多次調用都只會返回之前生成的_next。
由於then方法接受的兩個參數都是可選的,而且類型也沒限制,可以是函數,也可以是一個具體的值,還可以是另一個promise。下面是then的具體實現:
Promise.prototype.then = function(resolve, reject) { var next = this._next || (this._next = Promise()); var status = this.status; var x; if('pending' === status) { isFn(resolve) && this._resolves.push(resolve); isFn(reject) && this._rejects.push(reject); return next; } if('resolved' === status) { if(!isFn(resolve)) { next.resolve(resolve); } else { try { x = resolve(this.value); resolveX(next, x); } catch(e) { this.reject(e); } } return next; } if('rejected' === status) { if(!isFn(reject)) { next.reject(reject); } else { try { x = reject(this.reason); resolveX(next, x); } catch(e) { this.reject(e); } } return next; } };
這里,then做了簡化,其他promise類庫的實現比這個要復雜得多,同時功能也更多,比如還有第三個參數——notify,表示promise當前的進度,這在設計文件上傳等時很有用。對then的各種參數的處理是最復雜的部分,有興趣的同學可以參看其他類Promise庫的實現。
在then的基礎上,應該還需要至少兩個方法,分別是完成promise的狀態從pending到resolved或rejected的轉換,同時執行相應的回調隊列,即resolve()
和reject()
方法。
到此,一個簡單的promise就設計完成了,下面簡單實現下兩個promise化的函數:
function sleep(ms) { return function(v) { var p = Promise(); setTimeout(function() { p.resolve(v); }); return p; }; }; function getImg(url) { var p = Promise(); var img = new Image(); img.onload = function() { p.resolve(this); }; img.onerror = function(err) { p.reject(err); }; img.url = url; return p; };
由於Promise構造函數接受一個異步任務作為參數,所以getImg
還可以這樣調用:
function getImg(url) { return Promise(function(resolve, reject) { var img = new Image(); img.onload = function() { resolve(this); }; img.onerror = function(err) { reject(err); }; img.url = url; }); };
接下來(見證奇跡的時刻),假設有一個BT的需求要這么實現:異步獲取一個json配置,解析json數據拿到里邊的圖片,然后按順序隊列加載圖片,每張圖片加載時給個loading效果,
function addImg(img) { $('#list').find('> li:last-child').html('').append(img); }; function prepend() { $('<li>') .html('loading...') .appendTo($('#list')); }; function run() { $('#done').hide(); getData('map.json') .then(function(data) { $('h4').html(data.name); return data.list.reduce(function(promise, item) { return promise .then(prepend) .then(sleep(1000)) .then(function() { return getImg(item.url); }) .then(addImg); }, Promise.resolve()); }) .then(sleep(300)) .then(function() { $('#done').show(); }); }; $('#run').on('click', run)
這里的sleep只是為了看效果加的,可猛擊查看demo!
在這里,Promise.resolve(v)
靜態方法只是簡單返回一個以v為肯定結果的promise,v可不傳入,也可以是一個函數或者是一個包含then
方法的對象或函數(即thenable)。
類似的靜態方法還有Promise.cast(promise)
,生成一個以promise為肯定結果的promise;Promise.reject(reason)
,生成一個以reason為否定結果的promise。
我們實際的使用場景可能很復雜,往往需要多個異步的任務穿插執行,並行或者串行同在。這時候,可以對Promise進行各種擴展,比如實現Promise.all()
,接受promises隊列並等待他們完成再繼續,再比如Promise.any()
,promises隊列中有任何一個處於完成態時即觸發下一步操作。
3.標准的Promise
可參考html5rocks的這篇文章JavaScript Promises,目前高級瀏覽器如Chrome、Firefox都已經內置了Promise對象,提供更多的操作接口,比如Promise.all()
,支持傳入一個promises數組,當所有promises都完成時執行then,還有就是更加友好強大的異常捕獲,應對日常的異步編程,應該足夠了。
現今流行的各大js庫,幾乎都不同程度的實現了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出來的大都是Deferred
對象,當然還有angularJs中的$q.這里以jQuery為例,說一下:
// animate $('.box') .animate({'opacity': 0}, 1000) .promise() .then(function() { console.log('done'); }); // ajax $.ajax(options).then(success, fail); $.ajax(options).done(success).fail(fail); // ajax queue $.when($.ajax(options1), $.ajax(options2)) .then(function() { console.log('all done.'); }, function() { console.error('There something wrong.'); });
上面我們了解了Promise,相信大家對Promise有了一定的認識。下面我們開始動手來寫代碼,通過幾個簡單的例子,來加深理解。這里我們使用瀏覽器自帶的Promise,首先我們要先檢測一些瀏覽器是否支持Promise,其實很簡單,如果是谷歌瀏覽器,按下F12,打開控制台,如圖:
這里我們可以看到Promise的type是function,也就是說谷歌瀏覽器是支持promise的。以此為原理,我們可以寫一段JavaScript代碼來檢測,代碼如下:
if(typeof(Promise) === "function"){ alert("支持Promise"); } else{ alert("不支持Promise"); }
經過檢測,發現IE11竟然不支持promise.建議大家用谷歌瀏覽器來進行測試吧。
我們首先來寫一個等待的方法,如下:
function wait(duration){ return new Promise(function(resolve, reject) { setTimeout(resolve,duration); }) }
測試這個方法的代碼如下:wait(5000).then(function(){alert('hello')}),這段代碼很簡單,就是等待5秒以后執行一個回調,彈出一個消息。當然,你還可以這樣寫:
wait(5000).then(function(){alert('hello')}).then(function(){console.log('world')})
怎么樣?很簡單吧~~
下面來看一些我從網上收集的一些常用的JavaScript的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的回調里面拿到。
在比較復雜的頁面中,我們會使用到大量的異步操作。我們來看看使用Promise會帶來怎樣的便利吧~~
1、多個異步調用,同步/並行
例如我們頁面調用了好幾個異步函數,我們要等待所有的異步函數執行完成后,做一些操作,如彈出一個消息框提示用戶操作成功。下面我們拿一個例子來說明一下:
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 的異步寫法
如果用callback的話,我們就只能一個一個調用了,調用完了geta,然后在其回調函數里面調用getb,最后在getb的回調函數中調用print方法。串行和並行哪個更快,大家很清楚吧~~
2.競爭
如果說Primise.all是promise對象之間的“與”關系,那么Promise.race就是promise對象之間的“或”關系。比如,我要實現“點擊按鈕或者5秒鍾之后執行”:
var btn = document.getElementsByTagName('button'); Promise.race(wait(5000), waitFor(btn, click)).then(function(){ console.log('run!') })
3.異常處理
異常處理一直是回調的難題,而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){ //錯誤處理 })
4.復雜流程
接下來,我們來看比較復雜的情況。
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(); });
我們看到,不管Promise實現怎么復雜,但是它的用法卻很簡單,組織的代碼很清晰,從此不用再受callback的折磨了。promise作為一個新的API,它的API本身沒有什么特別的功能,但是它背后代表的編程思路是很有價值的。
最后,Promise是如此的優雅!但Promise也只是解決了回調的深層嵌套的問題,真正簡化JavaScript異步編程的還是Generator,在Node.js端,建議考慮Generator。
JavaScript Promise迷你書(中文版) http://liubin.github.io/promises-book/
JavaScript Promise啟示錄 http://www.csdn.net/article/2014-05-28/2819979-JavaScript-Promise
用Promise組織程序 http://www.w3ctech.com/topic/721
作者:雲霏霏
QQ交流群:243633526
博客地址:http://www.cnblogs.com/yunfeifei/
聲明:本博客原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關系。非商業,未授權,貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
如果大家感覺我的博文對大家有幫助,請推薦支持一把,給我寫作的動力。