要我能用得這麼熟,javascript
那前端出師了哈。前端
http://foio.github.io/javascript-asyn-pattern/java
改天一個一個親測一下。node
Javascript語言是單線程的,沒有複雜的同步互斥;可是,這並無限制它的使用範圍;相反,藉助於Node,Javascript已經在某些場景下具有通吃先後端的能力了。近幾年,多線程同步IO的模式已經在和單線程異步IO的模式的對決中敗下陣來,Node也所以得名。接下來咱們深刻介紹一下Javascript的殺手鐗,異步編程的發展歷程。git
讓咱們假設一個應用場景:一篇文章有10個章節,章節的數據是經過XHR異步請求的,章節必須按順序顯示。咱們從這個問題出發,逐步探求從粗糙到優雅的解決方案。github
在那個年代,javascript僅限於前端的簡單事件處理,這是異步編程的最基本模式了。 好比監聽dom事件,在dom事件發生時觸發相應的回調。ajax
element.addEventListener('click',function(){ //response to user click });
好比經過定時器執行異步任務。編程
setTimeout(function(){ //do something 1s later }, 1000);
可是這種模式註定沒法處理複雜的業務邏輯的。假設有N個異步任務,每個任務必須在上一個任務完成後觸發,因而就有了以下的代碼,這就產生了回調黑洞。json
doAsyncJob1(function(){ doAsyncJob2(function(){ doAsyncJob3(function(){ doAsyncJob4(function(){ //Black hole }); }) }); });
針對上文的回調黑洞問題,有人提出了開源的promise/A+規範,具體規範見以下地址:https://promisesaplus.com/。promise表明了一個異步操做的結果,其狀態必須符合下面幾個要求:後端
一個Promise必須處在其中之一的狀態:pending, fulfilled 或 rejected.
若是是pending狀態,則promise能夠轉換到fulfilled或rejected狀態。
若是是fulfilled狀態,則promise不能轉換成任何其它狀態。
若是是rejected狀態,則promise不能轉換成任何其它狀態。
promise有then方法,能夠添加在異步操做到達fulfilled狀態和rejected狀態的處理函數。
promise.then(successHandler,failedHandler);
而then方法同時也會返回一個promise對象,這樣咱們就能夠鏈式處理了。
promise.then(successHandler,failedHandler).then().then();
MDN上的一張圖,比較清晰的描述了Pomise各個狀態之間的轉換。
假設上文中的doAsyncJob都返回一個promise對象,那咱們看看如何用promise處理回調黑洞:
doAsyncJob1().then(function(){ return doAsyncJob2();; }).then(function(){ return doAsyncJob3(); }).then(function(){ return doAsyncJob4(); }).then(//......);
這種編程方式是否是清爽多了。咱們最常用的jQuery已經實現了promise規範,在調用$.ajax時能夠寫成這樣了:
var options = {type:'GET',url:'the-url-to-get-data'}; $.ajax(options).then(function(data){ //success handler },function(data){ //failed handler });
咱們可使用ES6的Promise的構造函數生成本身的promise對象,Promise構造函數的參數爲一個函數,該函數接收兩個函數(resolve,reject)做爲參數,並在成功時調用resolve,失敗時調用reject。以下代碼生成一個擁有隨機結果的promise。
var RandomPromiseJob = function(){ return new Promise(function(resolve,reject){ var res = Math.round(Math.random()*10)%2; setTimeout(function(){ if(res){ resolve(res); }else{ reject(res); } }, 1000) }); } RandomPromiseJob().then(function(data){ console.log('success'); },function(data){ console.log('failed'); });
jsfiddle演示地址:http://jsfiddle.net/panrq4t7/
promise錯誤處理也十分靈活,在promise構造函數中發生異常時,會自動設置promise的狀態爲rejected,從而觸發相應的函數。
new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).then(undefined,function(data){ console.log(data.message); });
其中then(undefined,function(data)能夠簡寫爲catch。
new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).catch(function(data){ console.log(data.message); });
jsfiddle演示地址:http://jsfiddle.net/x696ysv2/
promise的功能毫不僅限於上文這種小打小鬧的應用。對於篇頭提到的一篇文章10個章節異步請求,順序展現的問題,若是使用回調處理章節之間的依賴邏輯,顯然會產生回調黑洞; 而使用promise模式,則代碼形式優雅並且邏輯清晰。假設咱們有一個包含10個章節內容的數組,並有一個返回promise對象的getChaper函數:
var chapterStrs = [ 'chapter1','chapter2','chapter3','chapter4','chapter5', 'chapter6','chapter7','chapter8','chapter9','chapter10', ]; var getChapter = function(chapterStr) { return get('<p>' + chapterStr + '</p>', Math.round(Math.random()*2)); };
下面咱們探討一下如何優雅高效的使用promise處理這個問題。
順序promise主要是經過對promise的then方法的鏈式調用產生的。
//按順序請求章節數據並展現 chapterStrs.reduce(function(sequence, chapterStr) { return sequence.then(function() { return getChapter(chapterStr); }).then(function(chapter) { addToPage(chapter); }); }, Promise.resolve());
這種方法有一個問題,XHR請求是串行的,沒有充分利用瀏覽器的並行性。網絡請求timeline和顯示效果圖以下:
查看jsfiddle演示代碼: http://jsfiddle.net/81k9nv6x/1/
Promise類有一個all方法,其接受一個promise數組:
Promise.all([promise1,promise2,...,promise10]).then(function(){ });
只有promise數組中的promise所有兌現,纔會調用then方法。使用Promise.all,咱們能夠併發性的進行網絡請求,並在全部請求返回後在集中進行數據展現。
//併發請求章節數據,一次性按順序展現章節 Promise.all(chapterStrs.map(getChapter)).then(function(chapters){ chapters.forEach(function(chapter){ addToPage(chapter); }); });
這種方法也有一個問題,要等到全部數據加載完成後,纔會一次性展現所有章節。效果圖以下:
查看jsfiddle演示代碼:http://jsfiddle.net/7ops845a/
其實,咱們能夠作到併發的請求數據,儘快展現知足順序條件的章節:即前面的章節展現後就能夠展現當前章節,而不用等待後續章節的網絡請求。基本思路是:先建立一批並行的promise,而後經過鏈式調用then方法控制展現順序。
chapterStrs.map(getChapter).reduce(function(sequence, chapterStrPromise) { return sequence.then(function(){ return chapterStrPromise; }).then(function(chapter){ addToPage(chapter); }); }, Promise.resolve());
效果以下:
查看jsfiddle演示代碼:http://jsfiddle.net/fuog1ejg/
這三種模式基本上歸納了使用Pormise控制併發的方式,你能夠根據業務需求,肯定各個任務之間的依賴關係,從而作出選擇。
ES6中已經實現了promise規範,在新版的瀏覽器和node中咱們能夠放心使用了。對於ES5及其如下版本,咱們能夠藉助第三方庫實現,q(https://github.com/kriskowal/q)是一個很是優秀的實現,angular使用的就是它,你能夠放心使用。下一篇文章準備實現一個本身的promise。
異步編程的一種解決方案叫作"協程"(coroutine),意思是多個線程互相協做,完成異步任務。隨着ES6中對協程的支持,這種方案也逐漸進入人們的視野。Generator函數是協程在 ES6 的實現.
讓咱們先從三個方面瞭解generator。
在普通函數名前面加*號就能夠生成generator函數,該函數返回一個指針,每一次調用next函數,就會移動該指針到下一個yield處,直到函數結尾。經過next函數就能夠控制generator函數的執行。以下所示:
function *gen(){ yield 'I'; yield 'love'; yield 'Javascript'; } var g = gen(); console.log(g.next().value); //I console.log(g.next().value); //love console.log(g.next().value); //Javascript
next函數返回一個對象{value:'love',done:false},其中value表示yield返回值,done表示generator函數是否執行完成。這樣寫有點low?試試這種語法。
for(var v of gen()){ console.log(v); }
next()函數中能夠傳遞參數,做爲yield的返回值,傳遞到函數體內部。這裏有點tricky,next參數做爲上一次執行yeild的返回值。理解「上一次」很重要。
function* gen(x){ var y = yield x + 1; yield y + 2; return 1; } var g = gen(1); console.log(g.next()) // { value: 2, done: false } console.log(g.next(2)) // { value: 4, done: true } console.log(g.next()); //{ value: 1, done: true }
好比這裏的g.next(2),參數2爲上一步yield x + 1 的返回值賦給y,從而咱們就能夠在接下來的代碼中使用。這就是generator數據傳遞的基本方法了。
經過generator函數返回的指針,咱們能夠向函數內部傳遞異常,這也使得異步任務的異常處理機制獲得保證。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); console.log(g.next()); //{ value: 3, done: false } g.throw('error'); //error
仍然使用本文中的getChapter方法,該方法返回一個promise,咱們看一下如何使用generator處理異步回調。gen方法在執行到yield指令時返回的result.value是promise對象,而後咱們經過next方法將promise的結果返回到gen函數中,做爲addToPage的參數。
function *gen(){ var result = yield getChapter('I love Javascript'); addToPage(result); } var g = gen(); var result = g.next(); result.value.then(function(data){ g.next(data); });
gen函數的代碼,和普通同步函數幾乎沒有區別,只是多了一條yield指令。
jsfiddle地址以下:http://jsfiddle.net/fhnc07rq/3/
雖然gen函數自己很是乾淨,只須要一條yield指令便可實現異步操做。可是我卻須要一堆代碼,用於控制gen函數、向gen函數傳遞參數。有沒有更規範的方式呢?其實只須要將這些操做進行封裝,co庫爲咱們作了這些(https://github.com/tj/co)。那麼咱們用generator和co實現上文的逐步加載10個章節數據的操做。
function *gen(){ for(var i=0;i<chapterStrs.length;i++){ addToPage(yield getChapter(chapterStrs[i])); } } co(gen);
jsfiddle演示地址:http://jsfiddle.net/0hvtL6e9/
這種方法的效果相似於上文中提到「順序promise」,咱們能不能實現上文的「併發promise,漸進式」呢?代碼以下:
function *gen(){ var charperPromises = chapterStrs.map(getChapter); for(var i=0;i<charperPromises.length;i++){ addToPage(yield charperPromises[i]); } } co(gen);
jsfiddle演示地址: http://jsfiddle.net/gr6n3azz/1/
經歷過複雜性才能達到簡單性。咱們從最開始的回調黑洞到最終的generator,愈來愈複雜也愈來愈簡單。
===================
function *gen() { yield 'I'; yield 'love'; yield 'Javascript'; } var g = gen(); console.log(g.next().value); console.log(g.next().value); console.log(g.next().value); function *gen1(x) { var y = yield x + 1; yield y + 2; return 1; } var g1 = gen1(3); console.log(g1.next()); console.log(g1.next(10)); console.log(g1.next());