Node.js的一个特点是事件驱动,事件驱动在客户端很普遍,看看现在不知有多少人患上了鼠标手、键盘手,痛苦的是人,损坏的是鼠标键盘,头疼的是程序员们对onkeydown,onclick众多事件的处理。
先来看看网页上面常见的:
<input type="button" onclick="" onblur="" ........../>
上面的onclick和onblur就是事件,这个毫无疑问。服务器端不可能有click吧,这个也毫无疑问,那么服务器端谈事件有意思么?
诚然,在众多框架的层层封装下,socket等底层api根本不可能接触到,网络连接、断开等事件也无影无踪,最多就给你几个hook。
在Node.js中服务器端也可以用javascript了,在网页上可以用的,服务器端为什么不能用呢?下面我们先设想在服务器端实现onclick和onfocus事件吧:
首先,定义一个button对象:
var button = {
type : "button",
onclick : function(e){console.log("onclick");}, // click事件
onfocus : function(e){console.log("onfocus");}, // 获得焦点事件,click同时也会触发这个事件
onblur : function(e){console.log("onblur");}, // 如果button已经获得焦点,如果下一刻点击了其它的按钮,就会触发失去焦点的事件
position : {left : 3, top: 3, bottom : 7, right : 7}, // button的左上角、右下角的坐标
checkin : function(e) {return button.position.left <= e.mx && button.position.right >= e.mx
&& button.position.top <= e.my && button.position.bottom >= e.my;} , // 判断是否click了button
foucs : false // 按钮是否获得了焦点
};
再定义一个web浏览器对象:
var web = {
objs : [button], // 浏览器上只有一个button
click : function(e) { // 鼠标点击web
if (web.objs[0].checkin(e)) { // 判断鼠标是否点在button上了
web.objs[0].onclick(e); // 触发click事件
web.objs[0].onfocus(e); // 触发获得焦点事件
web.objs[0].foucs = true; // 设置button已经获得焦点
} else {
if (web.objs[0].foucs) { // button已获得焦点,如果此次鼠标没有点button,表示button失去了焦点
web.objs[0].onblur(e);
web.objs[0].foucs = false;
}
}
}
};
小鼠标上场了,假设它很疯狂,到处乱窜,而且每移动一步就click一下:
var mouse = {
move : function(){
var x = Math.random() * 10; //移动的范围限制在0-10之间。
var y = Math.random() * 10;
web.click({mx : x, my : y}); //click web
}
};
最后鼠标移动起来:
setInterval(mouse.move, 500);
从上面的例子中可以看到,一个完整的事件包含了:事件源(mouse),事件发射器(web),事件接收者(button)
在web端事件发射器也很少接触吧,除非写自定义控件、web游戏会写一些。嗯,既然这个有新鲜感,那么我们就从这开始探索node.js吧。
------------------------------------------------
放下上面的例子,先来看看node.js中的事件发射器:EventEmitter,这个类集发射、接受为一体,先来看看基本用法:
首先导入events包中的EventEmitter类
var eventEmitter = require("events").EventEmitter;
var emitter = new eventEmitter();
然后就可以定义要监听的事件了:
emitter.addListener("onclick", function() {console.log("onclick...");});
最后在某种条件下就可以发射了:
emitter.emit("onclick");
emitter根据发射事件的名字,找到addListener方法注册的监听器,打印onclick...
需要注意的是,事件名对大小写是敏感的。
这个类表面看起来很简单,实际上node.js中很多类的api看起来都相当地简洁。那么EventEmitter是不是没啥可玩的了。node.js号称是事件驱动的,应该还有,继续吧。
emitter.addListener()还有一个简化版本emitter.on():
var emitter = new eventEmitter();emitter.on("onclick", function() {console.log("onclick...");});
for(var i = 0; i < 3; i++)emitter.emit("onclick");
连续打印出三个log信息。这个addListener和on是如此地相同,有点蹊跷:
console.log(emitter.on == emitter.addListener)
log输出:
true
果然,两个函数其实质是同一个函数,on只是addListener函数的别名
在上个例子中如果只想打印一次log,可以用emitter.once():
var emitter = new eventEmitter();
emitter.once("onclick", function() {console.log("onclick...");});
for(var i = 0; i < 3; i++)emitter.emit("onclick");
如果一个事件挂两个监听器,会不会被冲掉:
emitter.on("onclick", function(){console.log("1 onclick");});
emitter.on("onclick", function(){console.log("2 onclick");});
emitter.emit("onclick");
log输出:
1 onclick
2 onclick
事件监听器原来是放在一个列表中的,而且执行的顺序与加入的顺序相关,先进先执行。
来一个带参数的:
emitter.on("onclick", function(info){console.log("1 onclick",info);});
emitter.on("onclick", function(info, info1){console.log("2 onclick",info, info1);});
emitter.on("onclick", function(){console.log("3 onclick");});
emitter.emit("onclick", ">>>");
log输出:
1 onclick >>>
2 onclick >>> undefined
3 onclick
参数传递很简单,在emit函数中传递就可以了,可以有一个参数,也可以有多个。
emitter.emit(event, [arg1],[arg2]....);
监听器中申明的参数也可以不跟emit中参数个数对应。还记得那个万能的Function.arguments么:
emitter.on("onclick",
function(){
for(var v in arguments) {
console.log(arguments[v]);
}
});
emitter.emit("onclick", "arg1", "arg2", ["arg3"], {arg4:"arg5"});
log输出:
arg1
arg2
[ 'arg3' ]
{ arg4: 'arg5' }
arguments是Function内建的对象,其中包含了所有传递给function的参数,使用的时候可以带Function名,也可以不带。这个设置挺好,如果强制带的话,匿名函数就不能用了。
既然function的参数这样随意,那么参数名都一样的时候会不会有问题:
emitter.on("onclick",
function (info, info){
console.log(info, info);
});
emitter.emit("onclick", "arg1", "arg2");
log输出:
arg2 arg2
程序没有问题,结果有问题了,这种写法相当于重复定义一个变量。
更进一步,如果function名和参数都一致:
emitter.on("onclick",
function info(info, info){
console.log(info, info);
});
emitter.emit("onclick", "arg1", "arg2");
log输出:
arg2 arg2
还是没有问题,赞,脚本语言之魅力。(哦,有点离题了)
既然emitter中监听器是放在一个列表中的,那么能不能在参数中直接加入一个列表呢:
function log(){console.log("onclick...");}
function log1(){console.log("1 onclick...");}
emitter.on("onclick",[log, log1]);
emitter.emit("onclick", "arg1", "arg2");
log输出:
throw TypeError('listener must be a function');
嗯,没有得逞,监听器只能是一个function。
EventEmitter 中还有几个函数可以获取某个事件的监听器列表:
function log(){console.log("onclick...");}
function log1(){console.log("1 onclick...");}
emitter.on("onclick",log);
emitter.on("onclick",log1);
console.log(emitter.listeners("onclick"));
emitter.listeners("onclick")[0].apply();
log输出:
[ [Function: log], [Function: log1] ]
onclick...
这个listeners函数返回监听器对象列表,EventEmitter还有个函数返回某个事件的监听器数:
var eventEmitter = require("events").EventEmitter;
var emitter = new eventEmitter();
function log(){console.log("onclick...");}
function log1(){console.log("1 onclick...");}
emitter.on("onclick",log);
emitter.on("onclick",log1);
console.log(eventEmitter.listenerCount(emitter, "onclick"));
log输出:
2
还有一个严肃的话题,事件是用一个字符串来表示的,如果是别人提供了一个EventEmitter,怎么知道它都有些什么事件呢。虽然可能有例子提供,但是如果例子不全或者根本就不提供,这个时候就太没有安全感了。在网上查了一下,找到一个api文档中没有说明的内部变量:emitter._events
function log(){console.log("onclick...");}
function log1(){console.log("1 onclick...");}
emitter.on("onclick",log);
emitter.on("onclick1",log1);
console.log(emitter._events);
for(var v in emitter._events)console.log("event name:[%s]", v);
log输出:
{ onclick: [Function: log], onclick1: [Function: log1] }
event name:[onclick]
event name:[onclick1]
有加必有减,emitter.removeListener(event, listener)可以把注册的事件监听器清除掉。
function log(){console.log("onclick...");}
emitter.on("onclick",log);
emitter.removeListener("onclick",log);
emitter.emit("onclick");
log无任何输出。
看看下面的例子:
emitter.on("onclick",function(){console.log("onclick");});
emitter.removeListener("onclick",function(){console.log("onclick");});
emitter.emit("onclick");
log输出:
onclick
监听器居然没有清除掉,第一次写例子的时候就犯了这个错误,想了一下也就明白了其中奥妙了。EventEmitter内部中,事件的监听器容纳在一个列表中的,removeListener第一步找到事件列表,然后搜索其中的监听器是否是参数中的function,然后在删除掉。上面的例子中,两个function是不相同的,当然无法remove。
如果想全部去除所有的事件或者某个事件的监听器,这个函数很有效:emitter.removeAllListeners([event])
呼,终于对照api文档将其用法介绍完了。感觉有点无聊,不过也没有办法,有些东西不亲自动手试一试,没法深入地理解。
将最开始写的例子用EventEmitter实现一下:
button对象不需要改变,其中变化最大的是web对象了,要利用EventEmitter,web必须继承EventEmitter。
在前面介绍过,继承的最简单方法是设置Function.prototype,所以web的写法都变更一下,将var web = {} 改成Function(){}
var emitter = require('events').EventEmitter; //导入EventEmitter
function webfunc() {
this.objs = [button]; // 浏览器上只有一个button
this.click = function(e) { // 鼠标点击web
if (web.objs[0].checkin(e)) { // 判断鼠标是否点在button上了
web.objs[0].onclick(e); // 触发click事件
web.objs[0].onfocus(e); // 触发获得焦点事件
web.objs[0].foucs = true; // 设置button已经获得焦点
} else {
if (web.objs[0].foucs) { // button已获得焦点,如果此次鼠标没有点button,表示button失去了焦点
web.objs[0].onblur(e);
web.objs[0].foucs = false;
}
}
}
}
webfunc.prototype = emitter.prototype; //继承EventEmitter
var web = new webfunc();
web.on("onclick", web.click); // 注册监听器
mouse需要小小的修改:
var mouse = {
move : function(){
var x = Math.random() * 10; //移动的范围限制在0-10之间。
var y = Math.random() * 10;
web.emit("onclick", {mx : x, my : y}); //click web
}
};
移动起来:
setInterval(mouse.move, 500);
运行正常。
其实node.js也提供了一个继承方法:
var util = require("util");
util.inherits(webfunc, emitter);
效果跟上面的webfunc.prototype = emitter.prototype;差不多
写起来就停不住了,内容比预期的要多,原本还想写一个http 服务器来练习一下EventEmitter,看来没地方了,只好开一个新篇章了。