Node.js編程之異步
異步操作
Node採用V8引擎處理JavaScript腳本,最大特點就是單線程運行,一次只能運行一個任務。這導致Node大量採用異步操作(asynchronous opertion),即任務不是馬上執行,而是插在任務隊列的尾部,等到前面的任務運行完後再執行。
由於這種特性,某一個任務的後續操作,往往採用回調函數(callback)的形式進行定義。
var isTrue = function(value, callback) { if (value === true) { callback(null, "Value was true."); } else { callback(new Error("Value is not true!")); } }
上面代碼就把進一步的處理,交給回調函數callback。
如果沒有發生錯誤,回調函數的第一個參數就傳入null。這種寫法有一個很大的好處,就是說只要判斷回調函數的第一個參數,就知道有沒有出錯,如果不是null,就肯定出錯了。另外,這樣還可以層層傳遞錯誤。
Node約定,如果某個函數需要回調函數作為參數,則回調函數是最後一個參數。另外,回調函數本身的第一個參數,約定為上一步傳入的錯誤對象。
var callback = function (error, value) { if (error) { return console.log(error); } console.log(value); }
異步開發的難題
在創建異步程序時,你必須密切關注程序的執行流程,並盯牢程序狀態:事件輪訓的條件、程序變量以及其他隨著程序邏輯執行而發生變化的資源。如果不小心,程序的變量也可能會出現意想不到的變化。下面這段代碼是一段因為執行順序而導致混亂的異步代碼。
如果例子中的代碼能夠同步執行,可以肯定輸出的應該是"The color is blue",可這個例子是異步的,在console.log執行前color的值還在變化,所以輸出是"The color is green".
function asyncFunction(callback) { setTimeout(callback, 200) } var color = 'blue' asyncFunction(function{ console.log('The color is ' + color) // The color is green.(這個最後執行(200ms之後)) }) color = 'green'
用JavaScript閉包可以"凍結"color的值,在如下代碼中,對asyncFunction的調用被封裝到了一個以color為參數的匿名函數裡,這樣就可以馬上執行這個匿名函數,把當前的color的值傳給它。而color變成了匿名函數的參數,也就是這個匿名函數內部的本地變量,當匿名函數外面的color值發生變化時,本地版的color不會受影響。
function asyncFunction(callback) { setTimeout(callback, 200) } var color = 'blue' (function(color) { asyncFunction(function{ console.log('The color is ' + color) // The color is blue. }) })(color); color = 'green
在Node開發中需要用到很多JavaScript編程技巧,這只是其中之一。
現在我們知道怎麼用閉包控制程序的狀態了,接下來我們看看怎麼讓異步邏輯順序執行。
異步流程的順序化
讓一組異步任務順序執行的概念被Node社區稱為流程控制。這種控制分為兩類:串行和並行,
什麼時候使用串行流程控制
可以使用回調讓幾個異步任務按順序執行,但如果任務很多,必須組織一下,否則會陷入回調地獄。
下面這段代碼就是用回調讓任務順序執行的。
setTimeout(function{ console.log('I execute first.') setTimeout(function{ console.log('I execute next.') setTimeout(function{ console.log('I execute last.') }, 100) }, 500) }, 1000)
此外,也可以用Promise這樣的流程控制工具來執行這些代碼
promise.then(function(result){ // dosomething return result; }).then(function(result) { // dosomething return promise1; }).then(function(result) { // dosomething }).catch(function(ex) { console.log(ex); }).finally(function{ console.log("final"); });
接著我們通過例子,自己來實現串行化流程控制和並行化流程控制
實現串行化流程控制
為了用串行化流程控制讓幾個異步任務按順序執行,需要先把這些任務按預期的執行順序放到一個數組中。如下圖所示:
下面是一個串行化流程控制的demo,實現了從隨機選擇的RSS預定源中獲取一篇文章的標題和URL,源文件
// 在一個簡單的程序中實現串行化流程控制 var fs = require('fs') var request = require('request') // 用它獲取RSS數據 var htmlparser = require('htmlparser') // 把原始的RSS數據轉換成JavaScript結構 var configFilename = './rss_feeds.txt' function checkForRSSFile { // 任務1:確保包含RSS預定源URL列表的文件存在 fs.exists(configFilename, function(exists) { if (!exists) { return next(new Error('Missing RSS file: ' + configFilename)) // 只要有錯誤就儘早返回 } next(null, configFilename) }) } function readRSSFile (configFilename) { // 任務2:讀取並解析包含預定源URL的文件 fs.readFile(configFilename, function(err, feedList) { if (err) { return next(err) } feedList = feedList // 講預定源URL列表轉換成字符串,然後分隔成一個數組 .toString .replace(/^\s+|\s+$/g, '') .split("\n"); var random = Math.floor(Math.random*feedList.length) // 從預定源URL數組中隨機選擇一個預定源URL next(null, feedList[random]) }) } // console.log('進入') function downloadRSSFeed(feedUrl) { // 任務3:向選定的預定源發送HTTP請求以獲取數據 request({uri: feedUrl}, function(err, res, body) { if (err) { return next(err) } if (res.statusCode != 200) { return next(new Error('Abnormal response status code')) } next(null, body) }) } function parseRSSFeed(rss) { // 任務4:將預定源數據解析到一個條目數組中 var handler = new htmlparser.RssHandler var parser = new htmlparser.Parser(handler) parser.parseComplete(rss) if (!handler.dom.items.length) { return next(new Error('No RSS items found')) } console.log(handler.dom.items) var item = handler.dom.items.shift console.log(item.title) console.log(item.link) } var tasks = [ checkForRSSFile, // 把所有要做的任務按執行順序添加到一個數組中 readRSSFile, downloadRSSFeed, parseRSSFeed ] function next(err, result) { if (err) { throw err } var currentTask = tasks.shift // 從任務數組中取出下個任務 if (currentTask) { currentTask(result) // 執行當前任務 } } next // 開始任務的串行化執行
如本例所示,串行化流程控制本質上是在需要時讓回調進場,而不是簡單地把它們嵌套起來
實現並行化流程控制
為了讓異步任務並行執行,仍然是要把任務放到數組中,但任務的存放順序無關緊要。每個任務都應該調用處理器函數增加已完成任務的計數值。當所有任務都完成後,處理器函數應該執行後續的邏輯。
來看一個並行化流程控制的小demo,該demo實現了在控制檯中統計打印出所有單詞分別出現的總數。源文件
// 在一個簡單的程序中實現並行流程控制 var fs = require('fs') var completedTasks = 0 var tasks = var wordCounts = {} var filesDir = './text' function checkIfComplete { // 當所有任務全部完成後,列出文件中用到的每個單詞以及用了多少次 completedTasks++ // console.log(completedTasks) console.log(tasks.length) if (completedTasks == tasks.length) { for(var index in wordCounts) { console.log(index + ': ' + wordCounts[index]) } } } function countWordsInText(text) { var words = text .toString .toLowerCase .split(/\W+/) .sort for (var index in words) { // 對文本中出現的單詞計數 var word = words[index] if (word) { wordCounts[word] = (wordCounts[word]) ? wordCounts[word] + 1 : 1 } } } fs.readdir(filesDir, function(err, files) { // 得出text目錄中的文件列表 if (err) { throw err } for(var index in files) { var task = (function(file) { // 定義處理每個文件的任務,每個任務中都會調用一個異步讀取文件的函數並對文件中使用的單詞計數 return function { fs.readFile(file, function(err, text) { // 這裡注意fs.readFile是一個異步進程,countWordsInText,checkIfComplete方法會在tasks.push方法後面進行 if (err) { throw err } countWordsInText(text) checkIfComplete }) } })(filesDir + '/' + files[index]) tasks.push(task) } for(var task in tasks) { tasks[task] } })
如上兩個demos闡述了串行和並行化流程控制的底層機制。
總結
可以用回調、事件發射器和流程控制管理異步邏輯。回調適用於一次性異步邏輯;事件發射器對組織異步邏輯很有幫助,因為它們可以把異步邏輯跟一個概念實體關聯起來,可以通過監聽器輕鬆管理;流程控制可以管理異步任務的執行順序,可以讓它們一個接一個執行,也可以同步執行。你可以自己實現流程管理,但社區附加模塊可以幫你解決這個麻煩。選擇哪個流程控制附加模塊很大程度取決於個人喜好以及項目或設計的需求。