1. 程式人生 > >[NodeJS]Node異步編程基礎

[NodeJS]Node異步編程基礎

add this 異步 異步任務 cfi content sse html模板 port

零、前言

  為什麽要用Node?

  Node把非阻塞IO作為提高應用性能的方式。而在JS中,天生擁有著異步編程機制: 事件機制。同時JS中不存在多進程。這樣當你執行相對較慢需要花費時間長的IO操作時並不會阻塞主進程的任務。

  在NodeJS中流行兩種響應邏輯的管理方式: 回調, 事件監聽。

回調通常用來定義一次性響應的邏輯。事件監聽器本質上也是一個回調,不同的是它跟事件相互關聯。

一、使用回調來處理一次性事件

回調是一個函數,被當做參數傳遞給異步函數,描述了異步操作完成後要做什麽。

案例: 創建一個簡單的http服務器實現如下功能

1. 異步獲取存放在JSON文件中的文章標題

2. 異步獲取簡單的HTML模版

3. 將文章標題組裝到HTML頁面中

4. 將HTML頁面發送給用戶

var http = require("http");
var fs = require("fs");
var srcFilename = "./titles.json";
var distFilename = "./index.html";

http.createServer(function(req, res){
    if(req.url == ‘/‘){
        fs.readFile(srcFilename, function(err, data){
            
if(err){ console.log(err); res.end("server end"); }else{ var titles = JSON.parse(data); fs.readFile(distFilename, "utf-8", function(err, data){ if(err){ console.log(err); res.end(
"server end"); }else{ var html = data.replace("%", titles.join("</li><li>")); res.writeHead(200, {"Content-type":"text/html"}); res.write(html); res.end(); } }); } }); } }).listen(8080, "127.0.0.1");

以上是NodeJS主程序,通過http模塊創建一個簡單的HTTP服務器。監聽指定端口8080和127.0.0.1主機地址。另外通過判斷request.url請求路徑來使用fs讀取不同文件中的內容,最後將這些內容填充到相應文本內容中,通過response響應對象將數據發送給用戶。json數據源和html模板文件如下:

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta id="viewport" name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
        <title>Node課程</title>
    </head>
    <body>
        <h1>最新Node課程內容:</h1>
        <ul>
            <li>%</li>
        </ul>
    </body>
</html>
[
    "NodeJS模塊原理講解",
    "NodeJS常用模塊簡單講解",
    "NodeJS異步編程基礎"
]

二、使用事件發射器處理重復性事件

通過給事件綁定一個觸發時的回調函數,那麽當事件發射器觸發事件時,會在事件觸發時執行這些回調函數。NodeJS中很多內置核心組件都是事件發射器的子類。如HTTP服務器,Net服務器和Stream流對象等。

簡單案例

var net = require("net");

var server = net.createServer(function(socket){
    socket.on("data", function(data){
        socket.write("輸入的內容是:");
        socket.write(data);
    });
});
server.listen(8888, "127.0.0.1");

監聽器可以針對某些特殊事件的監聽只調用一次事件處理的回調函數。使用 .once() 方式

接下來我們就來創建一個屬於自己的事件發射器。

首先我們先回顧一下events核心模塊 具體可以參考這篇文章: Events核心模塊講解

接下來創建一個簡單的發布訂閱系統,實現以下功能:

1. 用戶連接服務後,可以看到當前正處於連接狀態的其他用戶

2. 用戶連接服務後,可以給所有用戶發送消息

3. 用戶斷開服務後,系統會將該用戶從連接用戶池中移除

4. 處於某種原因需要暫停服務時,可以通過指定的命令停止服務

var events = require("events");
var net = require("net");

// 創建一個頻道發射器,用來管理所有的用戶,用戶的行為事件及其響應
var channel = new events.EventEmitter();
channel.clients = {};
channel.subscriptions = {};
channel.setMaxListeners(50);

// 註冊用戶連接服務的事件
channel.on(‘join‘, function(id, client) {
    channel.clients[id] = client;
    channel.subscriptions[id] = function(senderId, message){
        if(id != senderId){
            channel.clients[id].write(message);
        }
    };
    channel.on(‘broadcast‘, channel.subscriptions[id]);
    // 連接服務後先友好地提示當前房間內的人數
    var welcome = "Welcome! Guests online: " + this.listeners("broadcast").length;
    client.write(welcome);
});

// 註冊用戶斷開服務的事件
channel.on(‘leave‘, function(id) {
    // 移除該用戶的廣播消息事件響應
    channel.removeListener("broadcast", channel.subscriptions[id]);
    // 將消息廣播給其他用戶
    channel.emit("broadcast", id, id + " has left the chat.\n");
});

// 註冊暫停服務的事件
channel.on(‘shutdown‘, function() {
    // 先發消息提醒所有用戶,服務已經暫停
    channel.emit("broadcast", ‘‘, ‘Chat has shut down.\n‘);
    // 移除所有的廣播事件
    channel.removeAllListener("broadcast");
});

net.createServer(function(socket){
    var id = socket.remoteAddress + " : " + socket.remotePort;
    // 觸發用戶連接服務的事件
    channel.emit("join", id, socket);
    // 註冊用戶發送消息的事件
    socket.on(‘data‘, function(data){
        var data = data.toString();
        // 這裏先簡單設置為暫停服務的指令為shutdown
        if(data == "shutdown"){
            channel.emit("shutdown");
        }else{
            channel.emit("broadcast", id, data.toString());
        }
    });
    // 註冊用戶離開服務的事件
    socket.on(‘close‘, function() {
        channel.emit("leave", id);
    });
}).listen(8080, ‘127.0.0.1‘);

** 看了前面的兩個事件發射器案例,你還可以利用事件發射器來創建一個文件監聽器。

通常的做法都是創建一個JS類,通過繼承EventEmitter類來處理文件目錄下的所有文件。通過監視目錄中的文件變化從而將變化的文件進行處理。

var events = require("events");
var util = require("util");
var fs = require("fs");

function Watcher(watcherDir, processedDir){
    this.watcherDir = watcherDir;
    this.processedDir = processedDir;
}
util.inherit(Watcher, events.EventEmitter);

Watcher.prototype.watch = function(){
    var watcher = this;
    fs.readdir(watcher.watcherDir, function(err, files){
        if(err){
            throw err;
        }else{
            for(var index in files){
                if(files[index].isFile()){
                    watcher.emit("process", files[index]);
                }
            }
        }
    });
};

Watcher.prototype.start = function(){
    var watcher = this;
    fs.watchFile(watcherDir, function(){
        watcher.watch();
    });
};

// 創建這樣一個文件監聽器實例
var watchDir = "./watch";
var processedDir = "./done";
var watcher = new Watcher(watcherDir, processedDir);
// 註冊文件處理函數
watcher.on(‘process‘, function(file) {
    var watcherFile = this.watcherDir + "/" + file;
    var processedFile = this.processedDir + "/" + file.toLowerCase();
    // 通過重命名的方式來移動文件
    fs.rename(watchFile, processedFile, function(err){
        if(err){
            throw err;
        }
    });
});

watcher.start();

三、異步邏輯順序化

異步編程的代碼,回調越多,格式化的代碼形狀看起來就像格鬥遊戲中的角色發出的波。很顯然這是大家不願意看到的。

那麽如何讓異步任務能夠順序執行呢?程序流程控制被分為了兩類: 串行和並行。

串行就是任務一個接著一個的執行,執行完前一個才能執行接下來的一個。

串行化流程控制的本質在於如何將多個異步任務按照預期的順序放入一個數組隊列中。這樣當一個任務執行結束後會從隊列中取出下一個任務依次執行。

並行就是任務不需要一個接著一個來執行,而是可以交叉執行,使得任務看起來就像是同時在執行一樣。

var fs = require("fs");
var path = require("path");
// 已經完成的任務數
var completedTasks = 0;
// 待完成的任務數組
var tasks = [];
// 所有單詞的統計結果
var wordCounts = {};
// 讀取的目錄
var filesDir = ‘./text‘;

function checkIfComplete(){
    completedTasks++;
    if(completedTasks == tasks.length){
        for(var word in wordCounts){
            console.log("word = " + word + " ; count = " + wordCounts[word]);
        }
    }
}
function countWordsInText(text){
    var words = text.toString().toLowerCase().split(/\W+/);//.sort();
    console.log(words);
    for(var i in words){
        var word = words[i];
        if(word){
            wordCounts[word] = (wordCounts[word])? (wordCounts[word] + 1) : 1;
        }
    }
}

fs.readdir(filesDir, function(err, fileList){
    if(err){
        throw err;
    }else{
        for(var index in fileList){
            console.log(path.join(filesDir, fileList[index]));
            var task = (function(file){
                return function(){
                    fs.readFile(file, function(err, text){
                        if(err){
                            throw err;
                        }else{
                            countWordsInText(text);
                            checkIfComplete();
                        }
                    });
                };
            })(path.join(filesDir, fileList[index]));
            tasks.push(task);
        }
        for(var i in tasks){
            tasks[i]();
        }
    }
});

[NodeJS]Node異步編程基礎