在NodeJs中使用事件、监听器、定时器和回调

在阅读《Node.js+MongoDB+AngularJS Web开发》第四章的时候,发现很多概念以前没有去注意到,觉得还是有摘录下来的必要,方便以后查看。

Node.js的事件模型

Node.js应用程序在一个单线程的事件驱动模型中运行。

传统的线程网络模型中,请求进入一个Web服务器,并分配给一个可用的线程。对于请求的处理工作继续在该线程上运行,直到请求完成并发出响应。
使用线程模型在不同线程上处理两个请求.png
以上使用线程模型在不同线程上处理两个请求。

Node.js不是在各个线程为每个请求执行所有的工作。它是把工作添加到一个事件队列中,然后有一个单独的线程运行一个事件循环把这个工作提取出来。事件循环抓取时间队列中最上面的条目,执行它,然后抓取下一个条目。当执行长期运行或者有阻塞I/O的代码时,它不是直接调用该函数,而是把函数随同一个要在此函数完成后执行的回调一起添加到时间队列。当Node.js事件队列中的所有事件都被执行完成时,Node.js应用程序终止。
处理两个请求在一个事件驱动线程,使用Node.js事件模型.png
以上处理两个请求在一个事件驱动线程,使用Node.js的回调事件模型

当我们遇到阻塞I/O(读取文件、查询数据库、请求套接字、访问远程服务)的时候,阻塞I/O停止当前线程的执行并等待一个回应,直到收到回应才能继续。

Node.js使用事件回调来避免对阻塞I/O的等待。所以,阻塞I/O的任何请求都在后台的不同的线程中执行。Node.js在后台实现线程池。当该块的I/O从事件队列中检索一个事件时,Node.js从线程池中获取一个线程,并在那里执行功能,而不是主事件循环线程执行功能。这可以防止阻塞I/O阻碍事件队列的其余事件。在被阻塞的线程上执行的函数任然可以把事件添加到要处理的事件队列中。

事件循环要么在事件循环线程本身上执行功能,要么在一个单独的线程上执行功能,阻塞I/O采用后一种方式。
完整的Node.js事件模型.png
在Node.js事件模型中,工作作为一个带有回调的函数被添加到事件队列中,然后在事件循环线程中被提取出。之后,在无阻塞的情况下,在事件循环线程上执行该函数;或在阻塞的情况下,在一个单独的线程上执行它。

传统的线程模型遇到的问题:

  • 受到线程数量的限制。比如,你一次创建5个线程,当有6个访问同时到达时,只能让一个访问等待
  • 受到CPU数量的限制。一个CPU处理器只能处理一个线程

将工作添加到事件队列

在用Node.js进行开发的时候,要利用事件模型的可扩展性和性能,要确保把工作分解成可以作为一系列的回调来执行的块

可以使用以下的方法之一传递回调函数来在事件队列中调度工作:

  • 对内置的事件,如http.request或server.connection添加一个时间监听器
  • 创建自己的事件发射器并对它们添加自定义的监听器
  • 使用process.nextTick选项来调度在事件循环的下一次循环中被提取出的工作
  • 使用定时器来调度在特定的时间数量或每隔一段时间后要做的工作
  • 对阻塞I/O库调用之一做出调用,如写入文件或连接到一个数据库

定时器

3种类型的定时器:超时定时器、时间间隔定时器、即时定时器

  • 超时定时器:用于将工作延迟一个特定的时间数。当时间到了,执行回调函数,定时器消失。对于只执行一次的工作,可以使用超时定时器。 setTimeout方法可以实现。
  • 时间间隔定时器:用于按定期的延迟时间间隔执行工作。当延迟时间结束时,回调函数被执行,然后再次重新调度为该延迟时间。对于必须定期进行的工作,应该使用它。 setInterval方法可以实现。
  • 即时计时器:用来在I/O事件的回调函数开始执行后,但任何超时时间或时间间隔事件被执行之前,立刻执行工作。它们允许你把工作调度为在事件队列中的当前事件完成之后执行。使用即时计时器为其他回调产生长期运行的执行段,以防止I/O事件饥饿。setImmediate方法可以实现

取消定时器:在setTimeout和setInterval返回的对象中可用unref函数,它能够在这些事件是队列中仅有的事件是,通知事件循环不要继续。

myInterval = setInterval(myFunc)
myInterval.unref()

使用nextTick

在事件队列上调度工作的一个非常有用的方法是使用process.nextTick(callback)函数。此函数调度要在事件循环的下一次循环中运行的工作。nextTick()函数在I/O事件被触发之前执行。

var fs = require('fs')

fs.stat('nexttick.js',function(err,stats){
    if(stats){
        console.log('nexttick.js exists')
    }
})

setImmediate(function(){
    console.log('Immediate Timer 1 Executed')
})

setImmediate(function(){
    console.log('Immediate Timer 2 Executed')
})

process.nextTick(function(){
    console.log('Next Tick 1 Executed')
})

process.nextTick(function(){
    console.log('Next Tick 2 Executed')
})

事件发射器和监听器

下列代码演示了Node.js中实现监听器和自定义事件发生器。

var events = require('events')

function Account(){
    this.balance = 0
    events.EventEmitter.call(this)
    this.deposit = function(amount){
        this.balance += amount
        this.emit('balancechanged') //触发事件
    }
    this.withdraw = function(amount){
        this.balance -= amount
        this.emit('balancechanged') //触发事件
    }
}

Account.prototype.__proto__ = events.EventEmitter.prototype

function displayBalance(){
    console.log('Account balance: $%d',this.balance)
}

function checkOverdraw(){
    if(this.balance < 0){
        console.log('Account overdrawn !!!')
    }
}

function checkGoal(acc,goal){
    if(acc.balance > goal){
        console.log('goal achieved')
    }
}

var account = new Account()
account.on('balancechanged',displayBalance) //添加监听器
account.on('balancechanged',checkOverdraw)
account.on('balancechanged',function(){
    checkGoal(this,1000)
})
account.deposit(220)
account.deposit(320)
account.deposit(600)
account.withdraw(1200)

实现回调

回调的3个具体实现:将参数传递给回调函数、在循环内处理回调函数参数、嵌套回调

向回调函数传递额外的参数

使用回调时,常见的一个问题是如何从调用函数给回调函数传递额外的参数。做到这一点的方法是在一个匿名函数中实现该参数,然后用来自匿名函数的参数调用回调函数。

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','white']
    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('Aston Martin')

在回调中使用闭包

如果某个回调函数需要访问父函数的作用域的变量,就需要提供闭包,使这些值在回调函数从事件队列被提取出时可以得到。

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);
}

链式回调

使用异步函数时,如果两个函数都在事件队列上,那么我们无法保证它们的运行顺序。解决这一问题的最佳方法是让来自异步函数的回调再次调用该函数,直到没有更多的工作要做,以执行链式回调。

function logCar(car,callback){
    console.log('saw a %s',car)
    if(cars.length){
        process.nextTick(function(){
            callback()
        })
    }
}

function logCars(cars){
    var car = cars.pop()
    logCar(car,function(){
        logCars(cars)
    })
}

var cars = ['Ferrari', 'Porsche', 'Bugatti','Lamborghini','Aston Martin']
logCars(cars)