1. 程式人生 > >Javascript異步編程之二回調函數

Javascript異步編程之二回調函數

兩種 bsp lse div 輸入 引用 asc 捕獲 試驗

for (var i = 0; i < 5; i++)

{ setTimeout(function() { console.log(i); }, 1000);}

console.log(i);

我相信只要是做過前端筆試題的都見過這樣的題目,那麽輸出的結果是什麽呢?

第一種可能的答案:0 1 2 3 4 5 5
第二種可能的答案:5 5 5 5 5 5 5(後面每個5隔一秒輸出)
顯然第二種結果是正確的,接下來我們分析一下這個題目。首先看一下目前大家都在用的一個口令或者說方法:

同步優先、異步靠邊、回調墊底
用公式表達就是:同步 => 異步 => 回調


現在根據這個口令我們來分析一下結果為什麽是這個:
1)for循環和循環體外部的console是同步的,所以先執行for循環,再執行外部的console.log。(同步優先)
2)for循環裏面有一個setTimeout回調,他是墊底的存在,只能最後執行。(回調墊底)
那麽,為什麽我們最先輸出的是5呢?
這個也是非常好理解,for循環先執行,但是不會給setTimeout傳參(回調墊底),等for循環執行完,就會給setTimeout傳參,而外部的console打印出5是因為for循環執行完成了。

傳統的同步函數需要返回一個結果的話都是通過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() {
               你的代碼;
          })
     }
}
技術分享圖片

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

Javascript異步編程之二回調函數