標籤:llb ted yun ror span was -o exists exec
非同步作業
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 = 0var 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闡述了串列和並行化流程式控制制的底層機制。
總結
可以用回調、事件發射器和流程式控制制管理非同步邏輯。回調適用於一次性非同步邏輯;事件發射器對組織非同步邏輯很有協助,因為它們可以把非同步邏輯跟一個概念實體關聯起來,可以通過監聽器輕鬆管理;流程式控制制可以管理非同步任務的執行順序,可以讓它們一個接一個執行,也可以同步執行。你可以自己實現流程管理,但社區附加模組可以幫你解決這個麻煩。選擇哪個流程式控制制附加模組很大程度取決於個人喜好以及項目或設計的需求。
Node.js編程之非同步