node.js通过其强大的事件驱动模型提供了可扩展性和性能。了解事件模型至关重要,因为它可能迫使你改变设计应用程序的思维。
1. 了解Node.js事件模型
Node.js应用程序在一个单线程的事件驱动模型中运行。
1.1 比较事件回调和线程模型
- 传统的线程网络模型
请求进入一个web服务器,并被分配给一个可用的线程。对于该请求的处理工作继续在该线程上进行,直到请求完成并发出响应。
- Node.js事件模型的工作原理
Node.js不是在各个线程为每个请求执行所有的工作,反之,它把工作添加到一个事件队列中,然后有一个单独的线程运行一个事件循环把这个工作提取出来。
1.2 在Node.js中阻塞I/0
阻塞I/O的例子:
- 读取文件
- 查询数据库
- 请求套接字
- 访问远程服务
Node.js使用事件回调来避免对阻塞I/O的等待。因此,执行阻塞I/O的任何请求都在后台的不同的线程中执行。Node.js在后台实现线程池。当该块的I/O从时间队列中检测一个事件,Node.js从线程池中获取一个线程,并在那里执行功能,而不是主事件循环线程执行功能。这可以防止阻塞I/O阻碍事件队列中的其余事件。
2. 将工作添加到事件队列
在Node.js应用程序中,你可以使用下列方法之一传递回调函数来在事件队列中调度工作:
- 对阻塞I/O库调用之一做出调用
- 对内置的事件
- 创建自己的事件发射器并对它们添加自定义的监听器
- 使用process.nextTick选项来调度在事件循环的下一次循环中被提取出的工作
- 使用定时器来调度在特定时间数量或每隔一段时间后要做的工作
2.1 实现定时器
- 用超时时间来延迟工作
超时定时器用于将工作延迟一个特定时间数量。当时间到了,回调函数执行,而定时器消失。对于只需要执行一次的工作,你应该使用超时时间。
setTimeout( callback, delayMilliSeconds, [ args ] ) ;
setTimeout( myFunc, 1000 ) ;
setTimeout() 函数返回一个定时器对象的ID,你可以在delayMilliSeconds到期前的任何时刻把此ID传递给clearTimeout( timeoutID )来取消超时时间函数。
myTimeout = setTimeout( myFunc, 1000000 ) ;
clearTimeout( myTimeout ) ;
- 用时间间隔执行定期工作
时间间隔定时器用于按定期的延迟时间间隔执行工作。当延迟时间结束时,回调函数被执行,然后再次重新调度为该延迟时间。对于必须定期进行的工作,你可以使用时间间隔。
myInterval = setInterval( myFunc, 1000 ) ;
clearInterval( myInterval ) ;
- 使用即时计时器立即执行工作
即时计时器用来在I/O时间的回调函数开始执行后,但任何超时时间或时间间隔事件被执行之前,立刻执行工作。它们允许你把工作调度为在事件队列中的当前事件完成之后执行。你应该使用即时定时器为其他回调产生长期运行的执行段,以方式I/O饥饿。
myImmediate = setImmediate( myFunc ) ;
clearImmediate( myImmediate ) ;
- 从事件循环中取消定时器引用
当定时器事件回调是留在事件队列中的仅有事件时,通常你不会希望它们继续被调度。Node.js提供了一个非常有用的工具来处理这种情况。这个工具是在setInterval和setTimeout返回的对象中可用的unref()函数,它让你能够在这些事情是队列中仅有的事件时,通知事件循环不要继续。
myInterval = setInterval( myFunc ) ;
myInterval.unref() ;
可以重新引用:
myInterval.ref() ;
2.2 使用nextTick来调度工作
在事件队列上调度工作的一个非常有用的方法是使用process.nextTick( callback )函数。此函数调度要在事件循环的下一次循环中运行的工作。不想setImmediate()方法,nextTick()在I/O事件被触发之前执行。这可能会导致I/O事件的饥饿,所以Node.js通过默认值1000的process.maxTickDepth来限制事件队列的每次循环可执行的nextTick()事件的数目。
2.3 实现事件发射器和监听器
创建自己的自定义事件,以及实现当一个事件被发出时执行的监听器回调。
- 将自定义事件添加到Javascript对象
事件使用一个EventEmitter对象发出。这个对象包含在events模块中。emit( eventName, [ args ] ) 函数触发eventName事件,包含所提供的任何参数。
var events = require( 'events' ) ;
var emitter = new events.EventEmitter() ;
emitter.emit( 'simpleEvent' ) ;
直接把事件添加到你的Javascript对象,需要通过在对象实例中调用events.EventEmitter.call( this ) 来继承EventEmitter功能。你还需要把events.EventEmitter.prototype添加到对象的原型中。
function MyObj() {
Events.EventEmitter.call( this ) ;
}
MyObj.prototype.__proto__ = events.EventEmitter.prototype ;
var myObj = new MyObj() ;
myObj.emit( 'someEvent' ) ;
把事件监听器添加到对象
.addListener( eventName, callback ) : 将回调函数附加到对象的监听器中。每当eventName事件被触发时,回调函数就被重置在事件队列中执行。
- .on( eventName, callback ) : 同.addListener()
.once( eventName, callback ) : 只有eventName事件第一次被触发时,回调函数才被放置在事件队列中执行。
在对象中删除监听器
.listeners( eventName ) : 返回一个连接到eventName事件的监听器函数的数组
- .setMaxListerers( n ): 如果多余n的监听器都加入到EventEmitter对象,就触发警报。默认值是10
- .removeListener( eventName, callback ) : 将callback函数从EventEmitter对象的eventName事件中删除
3. 实现回调
- 将参数传递给回调函数
- 在循环内处理回调函数参数
- 以及嵌套回调
3.1 向回调函数传递额外的参数
大部分回调函数都有传递给它们的自动参数,如错误或结果缓冲区。使用回调时,常见的一个问题是如何从调用函数给他们传递额外的参数。做到这一点的方法是在一个匿名函数中实现该参数,然后用来自匿名函数的参数调用回调函数。
var events = require( 'events' ) ;
function CarShow() {
events.EventEmitter.call( this ) ;
this.seeCar = function( make ) {
this.emit( 'sawCar', make ) ;
}
}
CarShow.prototype.__proto__ = events.EventEmitter.prototype ;
var show = new CarShow() ;
function logCar( make ) {
console.log( 'Saw a ' + make ) ;
}
function logColorCar( make, color ) {
console.log( 'Saw a %s %s', color, make ) ;
}
show.on( 'sawCar', logCar ) ;
show.on( 'sawCar', function( make ) {
var colors = [ 'red', 'blue', 'black' ] ;
var color = colors[ Math.floor( Math.random() * 3 ) ] ;
logColorCar( make, color ) ;
} ) ;
show.seeCar( 'Ferrari' ) ;
show.seeCar( 'Porsche' ) ;
show.seeCar( 'Bugatti' ) ;
show.seeCar( 'Lamborghini' ) ;
show.seeCar( 'Astom Martin' ) ;
3.2 在回调中实现闭包
- 闭包:变量被绑定到一个函数的作用域,但不绑定到他的父函数的作用域。
当你执行一个异步回掉时,父函数的作用域可能改变,如果某个回调函数需要访问父函数的作用域的变量,就需要提供闭包,使这些值在回调函数从事件队列被提取出时,可以得到。
function logCar( logMsg, callback ) {
process.nextTick( function() {
callback( logMsg ) ;
} ) ;
}
var cars = [ 'Ferrari', 'Porsche', 'Bugatti' ] ;
for( var idx in cars ) {
var message = 'Saw a ' + cars[ idx ] ;
logCar( message, function() {
console.log( 'Normal Callback: ' + message ) ;
} ) ;
}
for( var idx in cars ) {
var message = 'Saw a ' + cars[ idx ] ;
( function( msg ){
logCar( msg, function() {
console.log( 'Closure Callback: ' + msg ) ;
} );
} )( message ) ;
}
3.3 链式回调
使用异步函数时,如果两个函数都在事件队列上,则你无法保证它们的运行顺序。解决这一问题的最佳方式是让来自异步函数的回调再次调用该函数,知道没有更多的工作要做,以执行链式回调。