04 nodejs的異步、回調、非阻塞IO、事件驅動
nodejs具有單線程、非阻塞io、事件驅動的特性,非阻塞io和單線程聯繫在一起,異步非阻塞的特性適合高併發場景,並且優於java和php等傳統後臺語言。具體請看前一篇文章的理解和分析:
異步程序的寫法
回調函數
nodejs的異步提供了強大的併發處理能力,但是在寫程序時需要注意,他的語法並不是像java和php一樣是從上到下執行的,具體看下邊的例子
//延時獲取數據
const asynGetData = function () {//錯誤的示範
let result = null;
setTimeout(() => {
result = "apple";
}, 1000);
return result;
}
console.log(asynGetData());//打印數據
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hVQ02ptg-1575854807018)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191208184358138.png)]
按照同步的寫法會發現程序的執行結果直接輸出了null而不是延時一秒打印出apple
,這是因爲setTimeout是一個異步函數,通俗的解釋是主線程執行到setTimout
函數時並不會真正的阻塞1秒鐘,而是直接把這個延時交給其他線程來做,主線程繼續向下執行,直到滿足延時1秒條件後,主線程繼續執行等待一秒完成後的回調函數:result = "apple";
,因此輸出結果是null。
那麼怎麼才能實現延時一秒獲取數據這個功能呢,這裏就得利用到回調函數了。(這裏指的是ES5之前的語法,ES6以後可以通過generator函數和promise以及await等語法糖實現同步寫異步功能)。
通過callback實現如下;
let aysnGetData = function (callback) {
let result = null;
setTimeout(() => {
result = "apple";
callback(result);
}, 1000);
}
function output(res) {//callback函數
console.log(res);
}
aysnGetData(output);
或者直接傳入回調函數
//通過過回調函數實現同步編寫異步功能方式二:直接傳入回調函數
let aysnGetData = function (callback) {
let result = null;
setTimeout(() => {
result = "apple";
callback(result);
}, 1000);
}
aysnGetData(function (res) {
console.log(res);
});
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Rt9MohTE-1575854807021)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191208184339111.png)]
之前的文章有個遺留的問題:
- 如何在異步函數執行完畢後進行下一步處理?也就是如何判斷異步函數執行完畢?
答案是通過callback。
events 模塊
除了上述的callback實現,nodejs還提供了events模塊來處理異步。event模塊提供EventEmitter方法,返回的實例具有監聽和發射功能,類似於設計模式中的監聽者模式。通過events模塊實現上邊延時獲取數據的功能。
let events = require("events");
let EventEmitter = new events.EventEmitter();
//監聽函數
EventEmitter.on("getDataOk", function (val) {
console.log("監聽到了 getDataOk 事件")
console.log("獲取數據成功,result的值是:", val);
})
setTimeout(() => {
console.log("開始觸發 getDataOk事件")
EventEmitter.emit("getDataOk", "apple")
}, 1000);
- new events.EventEmitter():生成 emitter實例對象
- on(事件標誌,callback):用於監聽事件,當監聽到指定標誌的時間後執行回調函數
- emit(事件標誌,要傳遞的值):發出事件並傳遞值。
有了events模塊就可以方便的進行異步處理了,在之前編寫簡單靜態web服務器中,用於通過擴展名獲取對應ContentType時,用到了同步讀文件函數readFileAsyn
,有了events模塊,我們也可以使用異步讀文件函數實現簡單web服務器了。
重構靜態web服務器
const fs = require("fs");
const url = require("url");
const http = require("http");
const path = require("path");
const events = require("events");
const mimeModal = require("./module/mimeModalByEvent.js")
const app = http.createServer(function (req, res) {
if (req.url == "/") {
req.url = "/index.html";
}
if (req.url == "/favicon.ico") {
return;
}
let pathname = url.parse(req.url).pathname;//獲取要請求的文件
console.log("請求的pathname是:", pathname);
let exname = path.extname(pathname);//獲取文件擴展名
console.log("文件擴展名是:", exname);
let fileLocation = "../../04.static_web/static/" + pathname;
let notFound = "../../04.static_web/static/404.html";
fs.readFile(fileLocation, (err, data) => {
if (err) {
console.log(404);
console.log(`文件${pathname}沒有在 ${fileLocation} 處找到!`)
res.writeHead(404, { "Content-Type": "text/html;charset='utf-8'" });
res.write(notFound);//發送404頁面
res.end();
} else {
// 獲取文件擴展名對應的ContentType
let eventEmitter = new events.EventEmitter();
mimeModal.getMime(eventEmitter, exname);
eventEmitter.on("readContentTypeOk", (val) => {
console.log("監聽到了readContentTypeOk事件,開始像瀏覽器返回數據");
res.writeHead(200, { "Content-Type": val + ";charset='utf-8'" });
res.write(data);
res.end();
})
}
})
});
app.listen(9999, "127.0.0.1")
console.log("server listen on 127.0.0.1:9999")
mimeModalByEvent.js文件代碼:
const fs = require("fs");
exports.getMime = function (eventEmitter, exname) {
fs.readFile("./mime.json", (err, data) => {
if (err) {
console.log("mime.json讀取失敗")
} else {
let contentType = JSON.parse(data.toString())[exname];
eventEmitter.emit("readContentTypeOk", contentType);
}
})
}
與之前的區別是用到了EventEmitter來通知函數文件讀取完畢
通過截圖可以看出,當nodejs接受到瀏覽器請求後,讀取文件交給其他線程來完成,主線程不阻塞讀取文件這裏,而是繼續接收請求,等文件讀取完畢後通過events庫來通知主線程返回對瀏覽器的響應。這就是nodejs異步非阻塞特點的具體體現。