node.js 之事件驱动

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,看来没地方了,只好开一个新篇章了。

你可能感兴趣的:(node.js 之事件驱动)