Javascript異步編程之二回調函數


上一節講異步原理的時候基本上把回掉函數也捎帶講了一些,這節主要舉幾個例子來具體化一下。在開始之前,首先要明白一件事,在javascript里函數可以作為參數進行傳遞,這里涉及到高階函數的概念,大家可以自行google一下。

 
傳統的同步函數需要返回一個結果的話都是通過return語句實現,例如:
 
function foo() {
     var a = 3,
          b = 2;
     return a+b;
}

var c = foo();
console.log(c); //5
 
就是說后面的代碼console.log要得到函數foo的運行結果只要調用該函數就可以得到它所返回的值a+b。
 
但是如果foo是一個異步函數,可以這樣做嗎?
 
異步函數的定義:
 
首先說一下javascript里面怎樣書寫異步函數。基本的方法就是,在你的函數定義里面調用別人已經提供的異步api (不管是原生的還是第三方的),你的函數也就是個異步函數了:
 
function foo(callback) {
     你自己的代碼;
     asyncFn(function() {
          var result = 你自己的代碼;
          callback(result);
     });
}

 

上面這個例子中,你要定義一個函數foo,里面調用了一個異步函數asyncFn,在asyncFn運行完了之后調用foo的回調函數callback,來對結果result進行處理。
 
上面是一般異步函數的定義格式。setTimeout是javascript里面經常用的異步api。 做試驗的時候經常用它來模擬異步操作,下面的例子模擬一個操作要運行1秒后才返回結果 (當然你可以設0秒,但仍然是異步的):
 
function foo(callback) {
     你自己的代碼;
     setTimeout(function() {
          var result = 你自己的代碼;
          callback(result);
     }, 1000);
}
 
在node.js里面提供了其他的api來起到類似的作用(把你的同步代碼寫成異步函數),setImmediate或者process.nextTick,其作用就是異步調用你的代碼。這兩個基本上用法類似,但特定情況下是不同的,可以上網自行查找setImmediate,setTimeout(fn, 0)和process.nextTick 的區別,一般在不了解的情況下,建議使用setImmediate.
 
function foo(callback) {
     你自己的代碼;
     setImmediate(function() {
          var result = 你自己的代碼;
          callback(result);
     });
}
 
function foo(callback) {
     你自己的代碼;
     process.nextTick(function() {
          var result = 你自己的代碼;
          callback(result);
     });
}

 

異步函數的調用:

以上是異步函數的定義,下面講一下異步函數是怎樣調用的。在調用異步函數的時候,回調函數有兩種寫法,一是直接寫個匿名回調函數,下面例子中function(data) {...} 就是匿名回掉函數:
 
foo(function(data) {
     你的代碼來使用傳回來的data;
});
 
二是先定義一個函數,然后使用函數引用作為回調:
 
function bar(data) {
     你的代碼來使用傳回來的data;
}

foo(bar);
 
現在回到開篇的問題,有同學一定看明白了,為什么異步函數不能用return來返回值。 下面把兩種寫法放一起,方便比較:
 
正確寫法:
function foo(callback) {
     你自己的代碼;
     asyncFn(function() {
          var result = 你自己的代碼;
          callback(result);
     });
}
 
錯誤寫法:
function bar() {
     你自己的代碼;
     asyncFn(function() {
          var result = 你自己的代碼;
     });
     
     return result;
}
 
在錯誤寫法中,bar企圖使用return來返回result。在上一篇已經講過,異步api不會等執行完了再往下執行,也就是說asyncFn在調用后馬上會往下執行return result這句,這時候asyncFn還在異步執行當中,result根本還沒有計算出來,所以不能return期望結果。當然對javascript語法比較熟的同學也清楚,函數外部不能訪問函數內部變量,就是說在asyncFn函數的外部是無法訪問到result這個變量的,不管那個是主要原因,哪個是次要,異步api是肯定不能用return來返回值。
 
以上就是基本的異步函數的定義和調用。
 
異步函數實例:
 
任何只講理論不講應用的教程都是耍流氓。。。好吧,舉個實際的例子:
 
網絡請求是node.js異步API中常用的一種,能夠進行網絡請求的node.js原生和第三方的api非常多,下面以superagent為例來演示一下(運行之前先用npm安裝superagent):
 
var agent = require('superagent');

agent.get('http://www.baidu.com')
     .end(function(err, res) {
          if(err) {
               console.log(err);
          } else {
               console.log('http status: ' + res.status);
               console.log(res.header);
          }
     });
 
上面代碼是可以直接運行的。 這個例子中agent.get(url).end(callback)是一個異步api調用, 在回調函數里面定義自己的代碼。 回掉函數傳回來兩個數據,err和res。 如果不出問題的話res對象是完整的http響應的內容,如果出錯的話出錯信息會保存在err對象中。
這里為了演示只是在控制台打印傳回來的res的status和header (注:superagent是個很不錯的http客戶端,可以去github了解其提供的具體功能)。
 
順帶提一下: 異步函數不能像同步代碼一樣用try...catch捕獲異常,所以這里有一個約定俗成,就是回掉函數一般需要有兩個參數,上面superagent的例子中就是err和res,一般第一個參數是error,當異步代碼發生異常的時候用這個參數返回異常詳細信息, 第二個參數才是返回的有用數據,當沒有異常的時候用它來返回,即superagent的例子中的res,返回的是http響應內容。
 
異步嵌套:
 
在同步代碼中,如果有三個函數順序執行,前一個的輸出作為后一個的輸入,只要按順序執行三個函數即可。但在異步代碼中,要做到這個就有點麻煩了。具體的原因就是因為不能像同步函數一樣用return來返回值,而必須用回掉函數。所以,第二個函數要在第一個函數的回調中調用,同樣第三個函數要在第二個函數的回調中調用,這就是所謂的回調嵌套。下面的例子是要先從a.txt讀取其內容,然后從b.txt讀取其內容,最后把兩個內容合並寫入ab.txt,完了后控制台打印:read and write done!
 
var fs = require('fs');

fs.readFile('./a.txt', function(err1, data1) {
     fs.readFile('./b.txt', function(err2, data2) {
          fs.writeFile('./ab.txt', data1 + data2, function(err) {
               console.log('read and write done!');
          });
     });
});
 
三個異步函數嵌套看起來挺簡單的。設想一下,如果有5個,10個甚至更多的異步函數要順序執行,那要嵌套多少層呢?(大家都不喜歡身材橫着長吧哈哈)說實話相當恐怖,代碼會變得異常難讀,難調試,難維護。這就是所謂的回調地獄或者callback hell。正是為了解決這個問題,才有了后面兩節要講的內容,用promise或generator進行異步流程管理。異步流程管理說白了就是為了解決回調地獄的問題。所以說任何事情都有兩面性,異步編程有它獨特的優勢,卻也同時遇到了同步編程根本不會有的代碼組織難題。
 
思考題:
 
自己寫異步函數還有一種經常遇到的情況,就是在循環中,比如for循環中不斷調用異步函數asyncFn,因為每次循環調用asyncFn你都不知道它會運行到什么時候,這種情況下你的foo函數什么時候調用回掉函數來返回最終結果? 大家可以用讀寫文件來試驗,使用fs.readFile這個函數來循環讀取某個目錄下面所有的文件內容,最后用fs.writeFile合並寫到一個新的文件里面。答案下回分解 :)
 
function foo(arr, callback) {
     for(var i=0; i<arr.length; ++i) {
          asyncFn(function() {
               你的代碼;
          })
     }
}

 

上一篇: Javascript異步編程之一異步原理

 

轉載請標明出處: http://www.cnblogs.com/chrischjh/p/4667713.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM