本文轉載自:衆成翻譯
譯者:網絡埋伏紀事
連接:http://www.zcfy.cc/article/1759
原文:https://blog.risingstack.com/node-hero-async-programming-in-node-js/node
本章我將指導你學習異步編程的原理,並向你展現如何在 JavaScript 和 Node.js 中實現異步編程。git
在傳統編程實踐中,大多數 I/O 操做都是同步發生的。若是想一想 Java,想一想如何用 Java 讀取一個文件,你會獲得下面這樣的代碼:github
try(FileInputStream inputStream = new FileInputStream("foo.txt")) { Session IOUtils; String fileContent = IOUtils.toString(inputStream); }
這背後發生了什麼?主線程會被阻塞,直到文件讀完,這意味着讀文件的同時其它什麼事情都作不了。要解決此問題,更好利用 CPU,就不得不手動管理線程。npm
若是有更多阻塞操做,那麼事件隊列就變得更糟糕:編程
(紅色塊表示在進程等待外部資源的響應而被阻塞時,黑色塊表示在代碼運行時,綠色塊表示應用的其他部分)數組
爲解決這個問題,Node.js 引入了一種異步編程模型。服務器
異步 I/O 是一種輸入/輸出處理的形式,它容許在傳輸完成以前,其它處理能繼續進行。網絡
在以下的示例中,我將展現 Node.js 中一個簡單的文件讀寫過程 - 同時採用同步和異步的方式,目的是向你展現經過避免阻塞應用程序,能實現什麼。app
下面咱們先從一個簡單的示例開始 - 以同步的方式用 Node.js 讀一個文件:異步
const fs = require('fs') let content try { content = fs.readFileSync('file.md', 'utf-8') } catch (ex) { console.log(ex) } console.log(content)
這裏剛剛發生了什麼?咱們試圖用 fs
模塊的同步接口讀一個文件。它按預期方式工做 - content
變量會包含 file.md
的內容。這種方式的問題是,Node.js 會被阻塞,直到操做完成 - 也就是說在文件正在被讀取時,它什麼事都作不了。
下面咱們看看如何修復!
咱們直到,在 JavaScript 中,異步編程只能用函數這個該語言的一等公民來實現:函數能夠像全部其它變量同樣傳給其它函數。將其它函數做爲參數的函數被稱爲高階函數。
以下是一個高階函數的最簡單示例:
const numbers = [2,4,1,5,4] function isBiggerThanTwo (num) { return num > 2 } numbers.filter(isBiggerThanTwo)
在上例中,咱們將一個函數傳遞給 filter 函數。經過這種方式咱們能夠定義過濾的邏輯。
這就是回調誕生的方式:若是你把一個函數傳遞給另外一個函數做爲參數,那麼就能夠在另外一個函數完成任務時,在該函數內調用傳進來的函數。不須要返回值,只用值調用另外一個函數。
這些所謂錯誤優先(error-first)的回調是 Node.js 自己的核心 - 核心模塊用了它,大多數 NPM 中的模塊也是。
const fs = require('fs') fs.readFile('file.md', 'utf-8', function (err, content) { if (err) { return console.log(err) } console.log(content) })
這裏要注意:
錯誤處理: 必須在回調中檢測錯誤,而不是用 try-catch
塊。
沒有返回值: 異步函數不返回值,可是值將被傳遞給回調。
下面咱們對這個文件作點修改,看看它其實是如何工做的:
const fs = require('fs') console.log('start reading a file...') fs.readFile('file.md', 'utf-8', function (err, content) { if (err) { console.log('error happened during reading the file') return console.log(err) } console.log(content) }) console.log('end of the file')
這段腳本的輸出將是:
start reading a file... end of the file error happened during reading the file
正如你所見,一旦咱們開始讀文件,執行繼續,應用程序打印出 end of the file
。一旦文件讀取完成,咱們的回調就只被調用一次。這怎麼可能呢?迎接事件循環。
事件循環是 Node.js / JavaScript 的核心 - 它負責安排異步操做。
在深刻了解以前,要確保理解什麼是事件驅動的編程。
事件驅動的編程是一種編程範式,在這種範式中程序流程是由事件決定的,好比用戶行爲(鼠標點擊、按鍵)、傳感器輸出或者其它程序/線程的消息。
實際上,它意味着應用程序按照事件行事。
而且,咱們在第一章已經學過,從開發者的觀點看,Node.js 是單線程的。這意味着沒必要處理線程和線程同步,Node.js 遠離了這種複雜性。除了你的代碼,全部東西都是並行執行的。
要更深刻理解事件循環,請繼續 youtube 上的視頻。
至此你已經對 JavaScript 中的異步編程工做機制有了一個基本認識,下面咱們來看看幾個如何組織代碼的示例。
爲避免所謂回調地獄,能夠作的一件事情是開始使用 async.js。
Async.js 幫助組織應用程序結構,讓流程控制更容易。
下面咱們看一個使用 Async.js 的簡短示例,而後用 Promises 重寫。
以下的代碼片斷映射三個文件上的狀態:
async.parallel(['file1', 'file2', 'file3'], fs.stat, function (err, results) { // 如今結果是是一個每一個文件的狀態數組 })
Promise 對象用於延遲及異步計算。一個 Promise 表明尚未完成,可是將來會執行的操做。
在實踐中,前面的示例能夠重寫爲以下:
function stats (file) { return new Promise((resolve, reject) => { fs.stat(file, (err, data) => { if (err) { return reject (err) } resolve(data) }) }) } Promise.all([ stats('file1'), stats('file2'), stats('file3') ]) .then((data) => console.log(data)) .catch((err) => console.log(err))
固然,若是使用一個有 Promise 接口的方法,那麼 Promise 示例的行數也會少不少。
下一章將會學習如何啓動第一個 Node.js HTTP 服務器。