浅谈Nodejs观察者模式


Posted in NodeJs onOctober 13, 2015

一、前言

Nodejs使用有些日子了,近来再回顾下其API、多使用新特性,以期有更高层次的掌握,本次API的总结区别于单纯对英文版的汉化,会多做些扩展和自己的理解,希望对大家有所帮助,先从最核心的Events开始

Nodejs的Events实现了一种观察者模式,其支持了Nodejs的核心机制,且http / fs / mongoose等都继承了Events,可以添加监听事件。这种设计模式在客户端的组件编程思想里经常会用到,我们先简单了解下该模式。

首次接触 观察者模式是在Extjs框架的 Ext.util.observable源码,那时刚接触js,感觉这种模式很强大,也是我最早接触到的设计模式,后来在 underscore.js 源码里也有看到,且后者实现更简捷、优雅,我编写组件时也基本是按照这种思想。

观察者模式就是为某一对象添加一监听事件,如on('show', callback),由该对象在符合条件如show时自行触发,浏览器本身已经为dom实现了监听机制。

如我们为input添加keyup监听,目的是为了输出其value

$( 'input' ).on( 'keyup', function(){
   console.log( this.value );
} );

这样输入内容时会自行在日志中输出其value。

但我们自己做一个组件如Dialog,如何监听最常用的show / hide事件呢?

初级的做法是实例化时直接将回调配置进去,如

var dialog = new Dialog({
  content: '这里是弹出框的内容',
  show: function(){
    console.log( '当弹框时输出此段内容' );
  }
});

这样也可以用,不过显然不够灵活,如何将dialog做的像input那样可随时添加事件呢

二、观察者模式实现

首先实现Events对象,这里提供基础的监听on和触发emit,事件是以json形式压栈在对象的_events里

var Events = {
  on: function( name, callback){
    this._events = this._events || {};
    this._events[ name ] = this._events[ name ] || [];
    this._events[ name ].push( callback );
  },
  emit: function( name ){
    this._events = this._events || {};
    var args = Array.prototype.slice.call( arguments, 1 ),
       me = this;
    if( this._events[ name ] ){
      $.each( this._events[ name ], function( k, v ){
        v.call( me, args );
      } )
    }
  }   
}

再抽象一个函数用于为对象复制属性

function extend( source ){
  var args = Array.prototype.slice.call( arguments, 1 );
  for( var i = 0, parent; parent = args[i]; i++ ){
    for( var prop in parent ){
      source[ prop ] = parent[ prop ];
    }
  }
}

实现一个Dialog,
仅实现创建; method: show / hide; event: show / hide;

看效果时,加上这段样式

.dialog{
  position: fixed;
  top: 50%;
  left: 50%;
  margin: -50px 0 0 -100px;
  width: 200px;
  height: 120px;
  background: #fff;
  border: 5px solid #afafaf;
}

实现组件

var Dialog = function( config ){
  this.config = config;
  this.init( this.config );
};

扩展属性

extend( Dialog.prototype, {

  init: function( config ){
    this.render( config )
  },

  render: function( config ){
    this.el = $( '<div>' ).addClass( 'dialog' );
    this.el.html( config.content );
    $( 'body' ).append( this.el );
  },

  show: function( param ){
    this.el.fadeIn();
    this.emit( 'show', param );
  },

  hide: function( param ){
    this.el.fadeOut();
    this.emit( 'hide', param );
  }

}, Events );

生成实例,并为其添加三个show及hide监听事件

var dialog = window.dialog = new Dialog({
  content: 'dialog one'
});

dialog.on( 'show', function( txt ){
  console.log( 'dialog show one ' + txt );
} );

//do something

dialog.on( 'show', function( txt ){
  console.log( 'dialog show two ' + txt );
} );

//do something

dialog.on( 'show', function( txt ){
  console.log( 'dialog show three ' + txt );
} );

//do something

dialog.on( 'hide', function( txt ){
  console.log( 'dialog hide one ' + txt );
} );

//do something

dialog.on( 'hide', function( txt ){
  console.log( 'dialog hide two ' + txt );
} );

//do something

dialog.on( 'hide', function( txt ){
  console.log( 'dialog hide three ' + txt );
} );

我们分六次添加了六个不同的show事件和hide事件。
当执行 dialog.show() 时就会输出三条对应的日志。添加的事件保存在 dialog._events里,如图

浅谈Nodejs观察者模式

添加的三个show都输出成功,事件保存在_events属性里

nodejs Events也是实现了这一过程。

三、结构

var Events = require( 'events' );
console.log( Events );
/*
输出如下数据,可以看出 Events指向其EventEmiter
{ [Function: EventEmitter]
  EventEmitter: [Circular],
  usingDomains: [Getter/Setter],
  defaultMaxListeners: 10,
  init: [Function],
  listenerCount: [Function] }
*/

var myEmitter = new Events();
console.log( myEmitter );
/*
{ domain: null,
  _events: {},   //可以看到实例本身也有_events属性,添加的监听的事件就保存在这里
  _maxListeners: undefined}
*/

console.log( myEmitter.__proto__ );
/*
{ domain: undefined,
  _events: undefined,
  _maxListeners: undefined,
  setMaxListeners: [Function: setMaxListeners],
  emit: [Function: emit],
  addListener: [Function: addListener],
  on: [Function: addListener],
  once: [Function: once],
  removeListener: [Function: removeListener],
  removeAllListeners: [Function: removeAllListeners],
  listeners: [Function: listeners] }
*/

myEmitter.on( 'show', function( txt ){ console.log( 'one ' + txt )})
myEmitter.on( 'show', function( txt ){ console.log( 'tow ' + txt )})
myEmitter.on( 'hide', function( txt ){ console.log( 'one ' + txt )})
myEmitter.emit( 'show', 'show' );
myEmitter.setMaxListeners( 10 );
console.log( myEmitter );
/*
{ domain: null,
  _events: { show: [ [Function], [Function] ], hide: [Function] }, //添加后的事情,以json形式存放
  _maxListeners: 10 }
*/

四、API

其提供的method有on,是addListener的简写都是为实例添加监听事件,其它属性也都顾名思义,就简单说明下

property
_events: undefined,   //以压栈形式存放on进来的事件
_maxListeners: undefined  //设置最大监听数,超出提warn

----------------------------------------------------------------------------------------------------------------

method
setMaxListeners: [Function: setMaxListeners], 
/*设置私有属性_maxListeners的值,默认Events会在当某监听事件多于10个时发现警告(见上面Events.defaultMaxListeners),以防止内存泄露,如
(node) warning: possible EventEmitter memory leak detected. 11 show listeners added. Use emitter.setMaxListeners() to increase limit.
但这只是个友好的提醒,可以通过设置最大监听数来规避这个问题
myEmitter.setMaxListeners( 20 );
*/

emit: [Function: emit],
 /*触发监听事件
emitter.emit( event, [arg1], [arg2], ... )
如myEmitter.on( 'show', 'prompt content' );
 参数1为事件名,参数二供on回调里的参数
 */

addListener: [Function: addListener],
 /*
添加监听事件
emitter.addListener( event, listener );
如 myEmitter.addListener( 'show', function( txt ){ console.log( txt ) } );
参数一是事件名,参数二是对应的回调,回调里的参数就是 emit里的arguments.prototype.slice.call(1);
 */

on: [Function: addListener],
 /*
是addListener简写
 */

once: [Function: once],
 /*
作用同 on,不过emit一次后就失效了
emitter.once( event, listener );
如 myEmitter.once( 'show', function( txt ){ console.log( txt ) } );
当myEmitter.emit执行第二次时没有输出
 */

removeListener: [Function: removeListener],
 /*
移除指定事件的指定回调,此时回调不能再用匿名函数。
emitter.removeListener( event, listener );
如 
function show( txt ){ console.log( txt ) };
myEmitter.on( 'show', show );
console.log( myEmitter._events ); 
// { show: [ Function: show ] }
myEmitter.removeListener( 'show', show );  
 console.log( myEmitter._events ); 
// {}
 */

removeAllListeners: [Function: removeAllListeners],
 /*
 删除指定事件的所有回调
 emitter.removeAllListeners( [ event ] );
 如 
  myEmitter.removeAllListeners( 'show' );   //删除所有show监听
  myEmitter.removeAllListeners();   //删除所有监听
 */

listeners: [Function: listeners]
/*
查看指定监听
emitter.listeners( event );
如 myEmitter.listeners( 'show' ); //返回一个数组
同我们前面使用的 myEmitter._events[ 'show' ]
*/

另外Events类本身提供了一个方法
Events.listenerCount( emitter, event ); 获取指定实例下指定监听数
如 Event.listenerCount( myEmitter, 'show' )

-----------------------------------------------------------------------------------------------

还有两个event
newListener / remoteListener,分别应用于为实例添加( on / once )和删除( removeListener ) 操作。
emitter.on( event, listener );
emitter.on( 'newListener', function( event, listener ){
  console.log( emitter.listeners( 'show' ) );   //注意,此时监听还并没有添加到 emitter.listeners
  console.log( arguments );  
 });

 emitter.on( 'removeListener', function(){
  console.log( emitter.listeners( 'show' ) );
  console.log( arguments );
 })

五、应用

使用Events,通常就直接实例化即可,如上面API部分所例

不过,如果我们在nodejs端也实现了一个组件,如前面的Dialog,如何让Dialog也具备Events的功能呢?可以用Extjs实现的 extend方案

创建Dialog构建器

var Dialog = function(){
  //do something
}

//抽象apply函数,提供属性的深度复制,同上面的extend
function apply( source ){
  var args = Array.prototype.slice.call( arguments, 1 );
  for( var i = 0, parent; parent = args[i]; i++ ){
    for( var prop in parent ){
      source[ prop ] = parent[ prop ];
    }
  }
}

//抽象extend函数,用于实现继承
var extend = function(){
  // inline overrides
  var io = function(o){
    for(var m in o){
      this[m] = o[m];
    }
  };
  var oc = Object.prototype.constructor;

  return function(sb, sp, overrides){
    if(typeof sp == 'object'){
      overrides = sp;
      sp = sb;
      sb = overrides.constructor != oc ? overrides.constructor : function(){sp.apply(this, arguments);};
    }
    var F = function(){},
      sbp,
      spp = sp.prototype;

    F.prototype = spp;
    sbp = sb.prototype = new F();
    sbp.constructor=sb;
    sb.superclass=spp;
    if(spp.constructor == oc){
      spp.constructor=sp;
    }
    sb.override = function(o){
      apply(sb, o);
    };
    sbp.superclass = sbp.supr = (function(){
      return spp;
    });
    sbp.override = io;
    apply(sb, overrides);
    sb.extend = function(o){return extend(sb, o);};
    return sb;
  };
}();

//将Events属性继承给Dialog
Dialog = extend( Dialog, Events );

//为Dialog新增 method show,其内触发 event show
Dialog.prototype.show = function( txt ){
  this.emit( 'show', txt );
}

var dialog = new Dialog();

//添加监听事件show
dialog.on( 'show', function(txt){ console.log( txt )});

//执行method show时,就会触发其内定义的show events,输出 this is show
dialog.show( 'this is show' );

这样就为一个组件实现了Events机制,当调用method时,会触发event

六、总结

nodejs提供了很好的监听机制,并且也应用在其所有模块,其支持了nodejs最特色的I/O模式,如我们启动http服务时会监听其 connect / close,http.request时会监听 data / end等,了解监听机制对学习理解nodejs的基础,也对提升编程思想有益。

NodeJs 相关文章推荐
nodejs中转换URL字符串与查询字符串详解
Nov 26 NodeJs
Nodejs实现多人同时在线移动鼠标的小游戏分享
Dec 06 NodeJs
Nodejs学习笔记之NET模块
Jan 13 NodeJs
NodeJS学习笔记之Http模块
Jan 13 NodeJs
Nodejs全局安装和本地安装的不同之处
Jul 04 NodeJs
nodejs如何获取时间戳与时间差
Aug 03 NodeJs
详解NodeJs支付宝移动支付签名及验签
Jan 06 NodeJs
深入理解nodejs中Express的中间件
May 19 NodeJs
详解Nodejs 通过 fs.createWriteStream 保存文件
Oct 10 NodeJs
nodejs简单访问及操作mysql数据库的方法示例
Mar 15 NodeJs
nodejs微信开发之授权登录+获取用户信息
Mar 17 NodeJs
Nodejs文件上传、监听上传进度的代码
Mar 27 NodeJs
使用Angular和Nodejs、socket.io搭建聊天室及多人聊天室
Aug 21 #NodeJs
nodejs创建web服务器之hello world程序
Aug 20 #NodeJs
windows下安装nodejs及框架express
Aug 07 #NodeJs
使用DNode实现php和nodejs之间通信的简单实例
Jul 06 #NodeJs
iPhone手机上搭建nodejs服务器步骤方法
Jul 06 #NodeJs
nodejs爬虫抓取数据之编码问题
Jul 03 #NodeJs
nodejs爬虫抓取数据乱码问题总结
Jul 03 #NodeJs
You might like
在任意字符集下正常显示网页的方法一
2007/04/01 PHP
php magic_quotes_gpc的一点认识与分析
2008/08/18 PHP
一个PHP数组应该有多大的分析
2009/07/30 PHP
PHP header函数分析详解
2011/08/06 PHP
php对象和数组相互转换的方法
2015/05/12 PHP
thinkphp3.x连接mysql数据库的方法(具体操作步骤)
2016/05/19 PHP
AJAX PHP无刷新form表单提交的简单实现(推荐)
2016/09/09 PHP
用dom+xhtml+css制作的一个相册效果代码打包下载
2008/01/24 Javascript
两个listbox实现选项的添加删除和搜索
2013/03/01 Javascript
JS定时器实例
2013/04/17 Javascript
JAVA四种基本排序方法实例总结
2015/07/24 Javascript
javascript实现简单的全选和反选功能
2016/01/05 Javascript
使用jQuery Ajax 请求webservice来实现更简练的Ajax
2016/08/04 Javascript
JS解决移动web开发手机输入框弹出的问题
2017/03/31 Javascript
vue项目中用cdn优化的方法
2018/01/03 Javascript
微信小程序基础教程之worker线程的使用方法
2019/07/15 Javascript
layui 实现表单和文件上传一起传到后台的例子
2019/09/16 Javascript
python3使用tkinter实现ui界面简单实例
2014/01/10 Python
Python解析网页源代码中的115网盘链接实例
2014/09/30 Python
Python的GUI框架PySide的安装配置教程
2016/02/16 Python
浅析AST抽象语法树及Python代码实现
2016/06/06 Python
Python实现的双色球生成功能示例
2017/12/18 Python
PyQt5每天必学之关闭窗口
2018/04/19 Python
python3 实现对图片进行局部切割的方法
2018/12/05 Python
解决pycharm运行程序出现卡住scanning files to index索引的问题
2019/06/27 Python
kafka-python 获取topic lag值方式
2019/12/23 Python
Django admin 实现search_fields精确查询实例
2020/03/30 Python
Python操作Excel把数据分给sheet
2020/05/20 Python
Python filter过滤器原理及实例应用
2020/08/18 Python
加拿大消费电子和手机购物网站:The Source
2017/01/28 全球购物
英国女性运动服品牌:Sweaty Betty
2018/11/08 全球购物
美国杰西潘尼官网:JCPenney
2019/06/12 全球购物
金融管理专业求职信
2014/07/10 职场文书
白酒代理协议书范本
2014/10/26 职场文书
考试没考好检讨书(精选篇)
2014/11/16 职场文书
华山导游词
2015/02/03 职场文书