零、前言
为什么要用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](); } } });