Skip to content

Latest commit

 

History

History
178 lines (126 loc) · 5.83 KB

timer.md

File metadata and controls

178 lines (126 loc) · 5.83 KB

定时器

”定时器“指的是Node的一些特定方法,可以让函数在指定时间执行。

Event Loop

“定时器”的实现是建立在“Event Loop”机制(中文译为“事件循环”)基础上的。所谓“Event Loop”是指Node的异步回调函数的处理机制。如果遇到异步操作,Node会把这些操作交给操作系统处理,自己继续往下执行。然后,等到空闲时,不断循环检查操作系统是否返回结果。一旦得到结果,就执行对应的回调函数。

“Event Loop”由Node底层的libuv库的uv_run函数实现,它的代码大致如下。

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    ...

    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    ...
}

每一轮事件循环,就会执行一次上面的代码。它的基本步骤如下。

  1. 更新当前时间(uv__update_time
  2. 执行setTimeoutsetIntervaluv__run_timers
  3. 执行(以前轮次的)定时器的回调函数(uv__run_pending
  4. 执行I/O事件的回调函数(uv__io_poll
  5. 执行setImmediateuv__run_check

这里需要注意的是,执行setTimeoutsetIntervalsetImmediate这三个方法时,它们指定的回调函数是不会在本轮事件循环执行的,而是会放入一个数组,在以后轮次的事件循环清空。

process.nextTick()

process.nextTick方法用于指定在本轮Event Loop即将结束、下轮Event Loop开始前执行的回调函数。因此,process.nextTick的回调函数会阻塞下一个Event Loop。所以,process.nextTick不能出现嵌套,否则会阻塞掉整个Event Loop,不过此时Node会报错。

var http = require('http');

function compute() {
  // performs complicated calculations continuously
  // ...
  process.nextTick(compute);
}

http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World');
}).listen(5000, '127.0.0.1');

compute();

上面代码中,服务器是不会响应HTTP请求的,因为嵌套的process.nextTick在网络I/O之前不断执行,不会结束。

实际使用时,需要分清process.nextTicksetImmediatesetTimeout(fn, 0)的执行顺序。

setImmediate(function () {console.log('setImmediate')});
process.nextTick(function () {console.log('nextTick')});
setTimeout(function () {console.log('setTimeout')}, 0);
// nextTick
// setTimeout
// setImmediate

上面代码中,nextTick之所以排在最前面,是因为它在本轮 Event Loop 的结尾执行,而setTimeout(fn, 0)setImmediate都是在下一轮 Event Loop 执行。

process.nextTick的一个应用是,确保回调函数异步执行。

function asyncReal(data, callback) {
  process.nextTick(function() {
    callback(data === 'foo');
  });
}

上面代码中,即使asyncReal同步执行,callback也能确保是异步执行。

另一个用途是保证某些方法在初始化之后执行。下面是一个数据流的库文件。

var EventEmitter = require('events').EventEmitter;

function StreamLibrary(resourceName) {
  this.emit('start');
  // ... 从文件读取数据,然后触发data事件
  this.emit('data', chunkRead);
}
StreamLibrary.prototype.__proto__ = EventEmitter.prototype;

上面这样的写法,使用时根本不会监听到start事件。

var stream = new StreamLibrary('fooResource');

stream.on('start', function() {
  console.log('Reading has started');
});

stream.on('data', function(chunk) {
  console.log('Received: ' + chunk);
});

上面代码中,start事件是监听不到的。因为StreamLibrary一初始化时,就会触发start事件,这时根本还没指定回调函数。这就需要使用process.nextTick改写StreamLibrary库。

function StreamLibrary(resourceName) {
  var self = this;

  process.nextTick(function() {
    self.emit('start');
  });

  // ... 从文件读取数据,然后触发data事件
  this.emit('data', chunkRead);
}

上面代码中,只有当前Event Loop的所有代码执行完,才会触发start事件,这就确保这个事件可以被监听到。

setImmediate()

setImmediate方法用于指定在下一轮 Event Loop 执行的回调函数。

setImmediate(callback[, arg][, ...])

它的第一个参数就是指定的回调函数,其他参数则会被传入回调函数。它返回一个对象,供clearImmediate()使用。

setImmediate指定的回调函数,执行顺序是在I/O事件的回调函数之后,setTimeoutsetInterval方法指定的回调函数(延迟时间非零的情况下)之前。

如果延迟时间为零,即setImmediatesetTimeout(fn, 0)哪个命令会先执行?答案是不确定。

var x = function () {
  setTimeout(function() {
    console.log('Timeout 0')
  }, 0);
};

var y = function () {
  setImmediate(function() {
    console.log('Immediate')
  });
};

setTimeout(function () {
  x();
  y();
}, 10);

上面代码执行后,Timeout 0Immediate都有可能首先输出。

考虑到setImmediate语义更清楚,行为更规范,建议总是使用它替代setTimeout(fn, 0)

clearImmediate()

clearImmediate方法用于清除setImmediate设置的定时器。它的参数是setImmediate方法返回的定时器对象。

参考链接