目錄javascript
衆所周知(這也忒誇張了吧?),Javascript經過事件驅動機制,在單線程模型下,以異步的形式來實現非阻塞的IO操做。這種模式使得JavaScript在處理事務時很是高效,但這帶來了不少問題,好比異常處理困難、函數嵌套過深。下面介紹幾種目前已知的實現異步操做的解決方案。
(操蛋,不支持TOC)html
這是最古老的一種異步解決方案:經過參數傳入回調,將來調用回調時讓函數的調用者判斷髮生了什麼。
直接偷懶上阮大神的例子:
假定有兩個函數f1和f2,後者等待前者的執行結果。
若是f1是一個很耗時的任務,能夠考慮改寫f1,把f2寫成f1的回調函數。前端
function f1(callback){ setTimeout(function () { // f1的任務代碼 callback(); }, 1000); }
執行代碼就變成下面這樣:
f1(f2);
採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。
回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂.也許你以爲上面的流程還算清晰。那是由於我等初級菜鳥還沒見過世面,試想在前端領域打怪升級的過程當中,遇到了下面的代碼:java
doA(function(){ doB(); doC(function(){ doD(); }) doE(); }); doF();
要想理清上述代碼中函數的執行順序,還真得停下來分析好久,正確的執行順序是doA->doF->doB->doC->doE->doD.
回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,程序的流程會很混亂,並且每一個任務只能指定一個回調函數。node
事件監聽模式是一種普遍應用於異步編程的模式,是回調函數的事件化,任務的執行不取決於代碼的順序,而取決於某個事件是否發生。這種設計模式常被成爲發佈/訂閱模式或者觀察者模式。
瀏覽器原生支持事件,如Ajax請求獲取響應、與DOM的交互等,這些事件天生就是異步執行的。在後端的Node環境中也自帶了events模塊,Node中事件發佈/訂閱的模式及其簡單,使用事件發射器便可,示例代碼以下:jquery
//訂閱 emitter.on("event1",function(message){ console.log(message); }); //發佈 emitter.emit('event1',"I am message!");
咱們也能夠本身實現一個事件發射器,代碼實現參考了《JavaScript設計模式與開發實踐》git
var event={ clientList:[], listen:function (key,fn) { if (!this.clientList[key]) { this.clientList[key]=[]; } this.clientList[key].push(fn);//訂閱的消息添加進緩存列表 }, trigger:function(){ var key=Array.prototype.shift.call(arguments),//提取第一個參數爲事件名稱 fns=this.clientList[key]; if (!fns || fns.length===0) {//若是沒有綁定對應的消息 return false; } for (var i = 0,fn;fn=fns[i++];) { fn.apply(this,arguments);//帶上剩餘的參數 } }, remove:function(key,fn){ var fns=this.clientList[key]; if (!fns) {//若是key對應的消息沒人訂閱,則直接返回 return false; } if (!fn) {//若是沒有傳入具體的回調函數,表示須要取消key對應消息的全部訂閱 fns&&(fns.length=0); }else{ for (var i = fns.length - 1; i >= 0; i--) {//反向遍歷訂閱的回調函數列表 var _fn=fns[i]; if (_fn===fn) { fns.splice(i,1);//刪除訂閱者的回調函數 } } } } };
只有這個事件訂閱發佈對象沒有多大做用,咱們要作的是給任意的對象都能添加上發佈-訂閱的功能:
在ES6中可使用Object.assign(target,source)
方法合併對象功能。若是不支持ES6能夠自行設計一個拷貝函數以下:es6
var installEvent=function(obj){ for(var i in event){ if(event.hasOwnProperty(i)) obj[i]=event[i]; } };
上述的函數就能給任意對象添加上事件發佈-訂閱功能。下面咱們測試一下,假如你家裏養了一隻喵星人,如今它餓了。github
var Cat={}; //Object.assign(Cat,event); installEvent(Cat); Cat.listen('hungry',function(){ console.log("鏟屎的,快把朕的小魚乾拿來!") }); Cat.trigger('hungry');//鏟屎的,快把朕的小魚乾拿來!
自定義發佈-訂閱模式介紹完了。
這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。web
ES6標準中實現的Promise是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。
所謂Promise
,就是一個對象,用來傳遞異步操做的消息。它表明了某個將來纔會知道結果的事件,而且這個事件提供統一的API,各類異步操做均可以用一樣的方法進行處理。
Promise
對象有如下兩個特色。
(1)對象的狀態不受外界影響。Promise
對象表明一個異步操做,有三種狀態:Pending
(進行中)、Resolved
(已完成,又稱Fulfilled)和Rejected
(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態
(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise
對象的狀態改變,只有兩種可能:從Pending
變爲Resolved
和從Pending
變爲Rejected
。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise
對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。
有了Promise
對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。
下面以一個Ajax請求爲例,Cnode社區的API中有這樣一個流程,首先根據accesstoken獲取用戶名,而後能夠根據用戶名獲取用戶收藏的主題,若是咱們想獲得某個用戶收藏的主題數量就要進行兩次請求。若是不使用Promise對象,以Jquery的ajax請求爲例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Promise</title> </head> <body> </body> <script type="text/javascript" src="http://apps.bdimg.com/libs/jquery/1.7.2/jquery.min.js"></script> <script type="text/javascript"> $.post("https://cnodejs.org/api/v1/accesstoken",{ accesstoken:"XXXXXXXXXXXXXXXXXXXXXXXXXXX" },function (res1) { $.get("https://cnodejs.org/api/v1/topic_collect/"+res1.loginname,function(res2){ alert(res2.data.length); }); }); </script> </html>
從上述代碼中能夠看出,兩次請求相互嵌套,若是改爲用Promise對象實現:
function post(url,para){ return new Promise(function(resolve,reject){ $.post(url,para,resolve); }); } function get(url,para){ return new Promise(function(resolve,reject){ $.get(url,para,resolve); }); } var p1=post("https://cnodejs.org/api/v1/accesstoken",{ accesstoken:"XXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }); var p2=p1.then(function(res){ return get("https://cnodejs.org/api/v1/topic_collect/"+res.loginname,{}); }); p2.then(function(res){ alert(res.data.length); });
能夠看到前面代碼中的嵌套被解開了,(也許有人會說,這代碼還變長了,坑爹嗎這是,請不要在乎這些細節,這裏僅舉例說明)。關於Promise對象的具體用法還有不少知識點,建議查找相關資料深刻閱讀,這裏僅介紹它做爲異步編程的一種解決方案。
關於Generator函數的概念能夠參考阮大神的ES6標準入門,Generator能夠理解爲可在運行中轉移控制權給其餘代碼,並在須要的時候返回繼續執行的函數,看下面一個簡單的例子:
function* helloWorldGenerator(){ yield 'hello'; yield 'world'; yield 'ending'; } var hw=helloWorldGenerator(); console.log(hw.next()); console.log(hw.next()); console.log(hw.next()); console.log(hw.next()); // { value: 'hello', done: false } // { value: 'world', done: false } // { value: 'ending', done: false } // { value: undefined, done: true }
Generator函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用Generator函數後,該函數並不執行,返回的也不是函數運行結果,而是一個遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next
方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
語句(或return
語句)爲止。換言之,Generator函數是分段執行的,yield
語句是暫停執行的標記,而next
方法能夠恢復執行。
Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield語句裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield語句下面,反正要等到調用next方法時再執行。因此,Generator函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。
若是有一個多步操做很是耗時,採用回調函數,可能會寫成下面這樣。
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
採用Promise改寫上面的代碼。(下面的代碼使用了Promise的函數庫Q)
Q.fcall(step1) .then(step2) .then(step3) .then(step4) .then(function (value4) { // Do something with value4 }, function (error) { // Handle any error from step1 through step4 }) .done();
上面代碼已經把回調函數,改爲了直線執行的形式,可是加入了大量Promise的語法。Generator函數能夠進一步改善代碼運行流程。
function* longRunningTask() { try { var value1 = yield step1(); var value2 = yield step2(value1); var value3 = yield step3(value2); var value4 = yield step4(value3); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }
若是隻有Generator函數,任務並不會自動執行,所以須要再編寫一個函數,按次序自動執行全部步驟。
scheduler(longRunningTask()); function scheduler(task) { setTimeout(function() { var taskObj = task.next(task.value); // 若是Generator函數未結束,就繼續調用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }, 0); }
在ES7(還未正式標準化)中引入了Async函數的概念,async函數的實現就是將Generator函數和自動執行器包裝在一個函數中。若是把上面Generator實現異步的操做改爲async函數,代碼以下:
async function longRunningTask() { try { var value1 = await step1(); var value2 = await step2(value1); var value3 = await step3(value2); var value4 = await step4(value3); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }
正如阮一峯在博客中所述,異步編程的語法目標,就是怎樣讓它更像同步編程,使用async/await的方法,使得異步編程與同步編程看起來相差無幾了。
隨着Node開發的流行,NPM社區中出現了不少流程控制庫能夠供開發者直接使用,其中很流行的就是async庫,該庫提供了一些流程控制方法,注意這裏所說的async並非標題五中所述的async函數。而是第三方封裝好的庫。其官方文檔見http://caolan.github.io/async/docs.html
async爲流程控制主要提供了waterfall(瀑布式)、series(串行)、parallel(並行)
function add(fn) { var num=100; var result=num+1; fn(result) } function minus(num,fn){ var result=num-2; fn(result); } function multiply(num,fn){ var result=num*3; fn(result); } function divide(num,fn){ var result=num/4; fn(result); } add(function (value1) { minus(value1, function(value2) { multiply(value2, function(value3) { divide(value3, function(value4) { console.log(value4); }); }); }); });
從上面的結果能夠看到回調嵌套很深。
2.使用async庫的流程控制
因爲後面的任務依賴前面的任務執行的結果,因此這裏要使用watefall方式。
var async=require("async"); function add(callback) { var num=100; var result=num+1; callback(null, result); } function minus(num,callback){ var result=num-2; callback(null, result); } function multiply(num,callback){ var result=num*3; callback(null, result); } function divide(num,callback){ var result=num/4; callback(null, result); } async.waterfall([ add, minus, multiply, divide ], function (err, result) { console.log(result); });
能夠看到使用流程控制避免了嵌套。
Web Worker是HTML5新標準中新添加的一個功能,Web Worker的基本原理就是在當前javascript的主線程中,使用Worker類加載一個javascript文件來開闢一個新的線程,起到互不阻塞執行的效果,而且提供主線程和新線程之間數據交換的接口:postMessage,onmessage。其數據交互過程也相似於事件發佈/監聽模式,異能實現異步操做。下面的示例來自於紅寶書,實現了一個數組排序功能。
頁面代碼:
<!DOCTYPE html> <html> <head> <title>Web Worker Example</title> </head> <body> <script> (function(){ var data = [23,4,7,9,2,14,6,651,87,41,7798,24], worker = new Worker("WebWorkerExample01.js"); worker.onmessage = function(event){ alert(event.data); }; worker.postMessage(data); })(); </script> </body> </html>
Web Worker內部代碼
self.onmessage = function(event){ var data = event.data; data.sort(function(a, b){ return a - b; }); self.postMessage(data); };
把比較消耗時間的操做,轉交給Worker操做就不會阻塞用戶界面了,遺憾的是Web Worker不能進行DOM操做。
參考文獻
Javascript異步編程的4種方法-阮一峯 《You Don't Know JS:Async&Performance》 《JavaScript設計模式與開發實踐》-曾探 《深刻淺出NodeJS》-樸靈 《ES6標準入門-第二版》-阮一峯 《JavaScript Web 應用開發》-Nicolas Bevacqua 《JavaScript高級程序設計第3版》