您的位置:首頁>正文

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闡述了串列和並行化流程控制的底層機制。

總結

可以用回檔、事件發射器和流程控制管理非同步邏輯。回檔適用於一次性非同步邏輯;事件發射器對組織非同步邏輯很有説明,因為它們可以把非同步邏輯跟一個概念實體關聯起來,可以通過監聽器輕鬆管理;流程控制可以管理非同步任務的執行順序,可以讓它們一個接一個執行,也可以同步執行。你可以自己實現流程管理,但社區附加模組可以幫你解決這個麻煩。選擇哪個流程控制附加模組很大程度取決於個人喜好以及項目或設計的需求。

但任務的存放順序無關緊要。每個任務都應該調用處理器函數增加已完成任務的計數值。當所有任務都完成後,處理器函數應該執行後續的邏輯。

來看一個並行化流程控制的小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闡述了串列和並行化流程控制的底層機制。

總結

可以用回檔、事件發射器和流程控制管理非同步邏輯。回檔適用於一次性非同步邏輯;事件發射器對組織非同步邏輯很有説明,因為它們可以把非同步邏輯跟一個概念實體關聯起來,可以通過監聽器輕鬆管理;流程控制可以管理非同步任務的執行順序,可以讓它們一個接一個執行,也可以同步執行。你可以自己實現流程管理,但社區附加模組可以幫你解決這個麻煩。選擇哪個流程控制附加模組很大程度取決於個人喜好以及項目或設計的需求。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示