#0 系列目录#html
Node核心思想: 1.非阻塞; 2.单线程; 3.事件驱动;
前端
在目前的web应用中,客户端和服务器端之间有些交互能够认为是基于事件的,那么AJAX就是页面及时响应的关键。每次发送一个请求时(无论请求的数据多么小),都会在网络里走一个来回。服务器必须针对这个请求做出响应,一般是开辟一个新的进程。那么越多用户访问这个页面,所发起的请求个数就会愈来愈多,就会出现内存溢出、逻辑交错带来的冲突、网络瘫痪、系统崩溃这些问题。node
Node和操做系统有一种约定,若是建立了新的连接,操做系统就将通知Node,而后进入休眠。若是有人建立了新的连接,那么它(Node)执行一个回调,每个连接只占用了很是小的(内存)堆栈开销。git
#1 Node.js的事件机制# Node.js在其Github代码仓库(https://github.com/joyent/node)上有着一句短短 的介绍:Evented I/O for V8 JavaScript。这句近似广告语的句子却道尽了 Node.js自身的特点所在:基于V8引擎实现的事件驱动IO
。在本文的这部份内容中,来揭开这Evented这个关键词的一切奥秘吧。程序员
Node.js可以在众多的后端JavaScript技术之中脱颖而出,正是因其基于事件的特色而受到欢迎
。拿Rhino来作比较,能够看出Rhino引擎支持的后端JavaScript摆脱不掉其余语言同步执行的影响,致使JavaScript在后端编程与前端编程之间有着十分显著的差异,在编程模型上没法造成统一。在前端编程中,事件的应用十分普遍,DOM上的各类事件
。在Ajax大规模应用以后,异步请求更获得普遍的认同,而Ajax亦是基于事件机制的
。在Rhino中,文件读取等操做,均是同步操做进行的。在这类单线程的编程模型下,若是采用同步机 制,没法与PHP之类的服务端脚本语言的成熟度媲美,性能也没有值得可圈可点的部分。直到Ryan Dahl在2009年推出Node.js后,后端JavaScript才走出其迷局。Node.js的推出,该变了两个情况:github
异步IO突破单线程编程模型的性能瓶颈
,使得JavaScript在后端达到实用价值。#2 事件机制的实现# Node.js中大部分的模块,都继承自Event模块
(http://nodejs.org/docs/latest/api/events.html)。Event模块 (events.EventEmitter)是一个简单的事件监听器模式的实现
。具备 addListener/on,once,removeListener,removeAllListeners,emit等基本的事件监听模式的方法实现。它与前端DOM树上的事件并不相同,由于它不存在冒泡,逐层捕获等属于DOM的事件行为,也没有preventDefault()、stopPropagation()、stopImmediatePropagation()等处理事件传递的方法。web
从另外一个角度来看,事件侦听器模式也是一种事件钩子(hook)的机制
,利用事件钩子导出内部数据或状态给外部调用者。Node.js中的不少对象,大多具备黑盒的特色,功能点较少,若是不经过事件钩子的形式,对象运行期间的中间值或内部状态,是咱们没法获取到的
。这种经过事件钩子的方式,可使编程者不用关注组件是如何启动和执行的,只需关注在须要的事件点上便可。数据库
var options = { host: 'www.google.com', port: 80, path: '/upload', method: 'POST' }; var req = http.request(options, function (res) { console.log('STATUS: ' + res.statusCode); console.log('HEADERS: ' + JSON.stringify(res.headers)); res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('BODY: ' + chunk); }); }); req.on('error', function (e) { console.log('problem with request: ' + e.message); }); // write data to request body req.write('data\n'); req.write('data\n'); req.end();
在这段HTTP request的代码中,程序员只须要将视线放在error,data这些业务事件点便可
,至于内部的流程如何,无需过于关注。编程
值得一提的是若是对一个事件添加了超过10个侦听器,将会获得一条警告
,这一处设计与Node.js自身单线程运行有关,设计者认为侦听器太多,可能致使内存泄漏
,因此存在这样一个警告。调用:后端
emitter.setMaxListeners(0);
能够将这个限制去掉。
其次,为了提高Node.js的程序的健壮性,EventEmitter对象对error事件进行了特殊对待。若是运行期间的错误触发了error事件,EventEmitter会检查是否有对error事件添加过侦听器,若是添加了,这个错误将会交由该侦听器处理,不然,这个错误将会做为异常抛出。若是外部没有捕获这个异常,将会引发线程的退出
。
#3 事件机制的进阶应用# ##3.1 继承event.EventEmitter## 实现一个继承了EventEmitter类是十分简单的,如下是Node.js中流对象继承 EventEmitter的例子:
function Stream() { events.EventEmitter.call(this); } util.inherits(Stream, events.EventEmitter);
Node.js在工具模块中封装了继承的方法,因此此处能够很便利地调用。程序员能够经过这样的方式轻松继承EventEmitter对象,利用事件机制,能够帮助你解决一些问题。
##3.2 多事件之间协做## 在略微大一点的应用中,数据与Web服务器之间的分离是必然的,如新浪微博、Facebook、Twitter等。这样的优点在于数据源统一,而且能够为相同数据源制定各类丰富的客户端程序。以Web应用为例,在渲染一张页面的时候,一般须要从多个数据源拉取数据,并最终渲染至客户端。Node.js在这种场景中能够很天然很方便的同时并行发起对多个数据源的请求
。
api.getUser("username", function (profile) { // Got the profile }); api.getTimeline("username", function (timeline) { // Got the timeline }); api.getSkin("username", function (skin) { // Got the skin });
Node.js经过异步机制使请求之间无阻塞,达到并行请求的目的,有效的调用下层资源
。可是,这个场景中的问题是对于多个事件响应结果的协调并不是被Node.js原生优雅地支持
。为了达到三个请求都获得结果后才进行下一个步骤
, 程序也许会被变成如下状况:
api.getUser("username", function (profile) { api.getTimeline("username", function (timeline) { api.getSkin("username", function (skin) { // TODO }); }); });
这将致使请求变为串行进行,没法最大化利用底层的API服务器。
为解决这类问题,我曾写做一个模块(EventProxy,https://github.com/JacksonTian/eventproxy)来实现多事件协做
,如下为上面代码的改进版:
var proxy = new EventProxy(); proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) { // TODO }); api.getUser("username", function (profile) { proxy.emit("profile", profile); }); api.getTimeline("username", function (timeline) { proxy.emit("timeline", timeline); }); api.getSkin("username", function (skin) { proxy.emit("skin", skin); });
EventProxy也是一个简单的事件侦听者模式的实现
,因为底层实现跟Node.js的EventEmitter不一样,没法合并进Node.js中。可是却提供了比EventEmitter更强大的功能,且API保持与EventEmitter一致,与Node.js的思路保持契合,并能够适用在前端中。
这里的all方法是指侦听完profile、timeline、skin三个方法后,执行回调函数,并将侦听接收到的数据传入
。
最后还介绍一种解决多事件协做的方案:
Jscex(https://github.com/JeffreyZhao/jscex)。Jscex经过运行时编译的思路 (须要时也可在运行前编译),将同步思惟的代码转换为最终异步的代码来执行
,能够在编写代码的时候经过同步思惟来写,能够享受到同步思惟的便利写做,异步执行的高效性能。若是经过Jscex编写,将会是如下形式:
var data = $await(Task.whenAll({ profile: api.getUser("username"), timeline: api.getTimeline("username"), skin: api.getSkin("username") })); // 使用data.profile, data.timeline, data.skin // TODO
##3.3 利用事件队列解决雪崩问题## 所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库没法同时承受如此大的查询请求,进而往前影响到网站总体响应缓慢
。那么在Node.js中如何应付这种情景呢。
var select = function (callback) { db.select("SQL", function (results) { callback(results); }); };
以上是一句数据库查询的调用,若是站点恰好启动,这时候缓存中是不存在数 据的,而若是访问量巨大,同一句SQL会被发送到数据库中反复查询,影响到 服务的总体性能。一个改进是添加一个状态锁
。
var status = "ready"; var select = function (callback) { if (status === "ready") { status = "pending"; db.select("SQL", function (results) { callback(results); status = "ready"; }); } };
可是这种情景,连续的屡次调用select发,只有第一次调用是生效的,后续的select是没有数据服务的
。因此这个时候引入事件队列吧:
var proxy = new EventProxy(); var status = "ready"; var select = function (callback) { proxy.once("selected", callback); if (status === "ready") { status = "pending"; db.select("SQL", function (results) { proxy.emit("selected", results); status = "ready"; }); } };
这里利用了EventProxy对象的once方法,将全部请求的回调都压入事件队列中,并利用其执行一次就会将监视器移除的特色,保证每个回调只会被执行一次
。对于相同的SQL语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的调用,只需在队列中等待数据就绪便可,节省了重复的数据库调用开销。因为Node.js单线程执行的缘由,此处无需担忧状态问题。这种方式其实也能够应用到其余远程调用的场景中,即便外部没有缓存策略,也能有效节省重复开销。此处也能够用EventEmitter替代EventProxy,不过 可能存在侦听器过多,引起警告,须要调用setMaxListeners(0)移除掉警告,或者设更大的警告阀值
。
#4 简单事件示例## 由于Node采用的是事件驱动的模式
,其中的不少模块都会产生各类不一样的事件,可由模块来添加事件处理方法,全部可以产生事件的对象都是事件模块中的EventEmitter类的实例
。代码是全世界通用的语言,因此咱们仍是用代码说话:
// 使用require()方法添加了events模块并把返回值赋给了一个变量 var events = require("events"); // 建立了一个事件触发器,也就是所谓的事件模块中的 EventEmitter 类的实例 var emitter = new events.EventEmitter(); // on(event, listener)用来为某个事件 event 添加事件处理方法监听器 emitter.on("myEvent", function(msg) { console.log(msg); }); // emit(event, [arg1], [arg2], [...]) 方法用来产生事件。以提供的参数做为监听器函数的参数,顺序执行监听器列表中的每一个监听器函数。 emitter.emit("myEvent", "Hello World.");
EventEmitter 类中的方法都与事件的产生和处理相关:
addListener(event, listener) 和 on(event, listener) 这两个方法都是将一个监听器添加到指定事件的监听器数组的末尾
once(event, listener) 这个方法为事件为添加一次性的监听器。该监听器在事件第一次触发时执行,事后将被移除
removeListener(event, listener) 该方法用来将监听器从指定事件的监听器数组中移除出去
emit(event, [arg1], [arg2], [...]) 刚刚提到过了。
在Node中,存在各式各样不一样的数据流,Stream(流)是一个由不一样对象实现的抽象接口
。例如请求HTTP服务器的request是一个流,相似于stdout(标准输出);包括文件系统、HTTP 请求和响应、以及 TCP/UDP 链接等。流能够是可读的,可写的,或者既可读又可写。全部流都是EventEmitter的实例,所以能够产生各类不一样的事件
。可读流主要会产生如下事件:
data 当读取到流中的数据时,此事件被触发
end 当流中没有数据可读时,此事件被触发
error 当读取数据出现错误时,此事件被触发
close 当流被关闭时,此事件被触发,但是并非全部流都会触发这个事件。(例如,一个链接进入的HTTP request流就不会触发'close'事件。)
还有一种比较特殊的 fd 事件,当在流中接收到一个文件描述符时触发此事件
。只有UNIX流支持这个功能,其余类型的流均不会触发此事件。