理解Node.js里的process.nextTick()

Nodejs的事件驱动,异步IO模型另异步编程变得异常风行,它借助了异步IO模型及V8高性能引擎,突破了单线程的性能瓶颈. 让Jacascript在后端达到了其应有的使用价值.

另外一方面,它也统一了前后端的javascript编程模型.

异步编程也给前端程序猿带来了诸多的痛苦和不适应. 这也是我今天着重想提process.nextTick()这个API的原因之一.

异常处理

在过去,我们处理异常一般使用try catch fanily这个语句块进行异常捕获和处理

1
2
3
4
5
try {
JSON.parse(jsonObj)
} catch(e) {
// do something...
}

但是这个方法对异步编程而言是并不适用的, 举个很简单的例子,也是我们在日常开发中常常使用的一个小技巧: 当我们想要某个在文档中间的语句块在整个脚本的最后执行的时候,可以使用setTimeout将这个语句块包起来,将延时时间设为0, 那么这个时候它就会等待所有同步的进程全部执行完毕之后再执行.

在try中,我们进行异步的方式处理JSON.parse的时候,那么它也会被放到文档流的最后执行而try catch先执行了,这就是导致出现这个问题的原因

那么这个时候,就需要们的主角登场了

同样的,我们再来一个例子

1
2
3
4
5
function foo() {
console.error('foo');
}
process.nextTick(foo);
console.error('bar');

运行上面的代码,你从下面终端打印的信息会看到,”bar”的输出在“foo”的前面。这就验证了上面的说法,foo()是在下一个时间点运行的。

但在内部的处理机制上,process.nextTick()和setTimeout(fn, 0)是不同的,process.nextTick()不是一个单纯的延时,他有更多的 特性。

更精确的说,process.nextTick()定义的调用会创建一个新的子堆栈。在当前的栈里,你可以执行任意多的操作。但一旦调用netxTick,函数就必须返回到父堆栈。然后事件轮询机制又重新等待处理新的事件,如果发现nextTick的调用,就会创建一个新的栈。

下面我们来看看,什么情况下使用process.nextTick():

在多个事件里交叉执行CPU运算密集型的任务:

在下面的例子里有一个compute(),我们希望这个函数尽可能持续的执行,来进行一些运算密集的任务。

但与此同时,我们还希望系统不要被这个函数堵塞住,还需要能响应处理别的事件。这个应用模式就像一个单线程的web服务server。在这里我们就可以使用process.nextTick()来交叉执行compute()和正常的事件响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();

在这种模式下,我们不需要递归的调用compute(),我们只需要在事件循环中使用process.nextTick()定义compute()在下一个时间点执行即可。在这个过程中,如果有新的http请求进来,事件循环机制会先处理新的请求,然后再调用compute()。反之,如果你把compute()放在一个递归调用里,那系统就会一直阻塞在compute()里,无法处理新的http请求了。你可以自己试试。

当然,我们无法通过process.nextTick()来获得多CPU下并行执行的真正好处,这只是模拟同一个应用在CPU上分段执行而已。

保持回调函数异步执行的原则

当你给一个函数定义一个回调函数时,你要确保这个回调是被异步执行的。下面我们看一个例子,例子中的回调违反了这一原则:

1
2
3
4
5
6
7
8
function asyncFake(data, callback) {  
if(data === 'foo') callback(true);
else callback(false);
}

asyncFake('bar', function(result) {
// this callback is actually called synchronously!
});

为什么这样不好呢?我们来看Node.js 文档里一段代码:

1
2
3
4
var client = net.connect(8124, function() { 
console.log('client connected');
client.write('world!\r\n');
});

在上面的代码里,如果因为某种原因,net.connect()变成同步执行的了,回调函数就会被立刻执行,因此回调函数写到客户端的变量就永远不会被初始化了。

这种情况下我们就可以使用process.nextTick()把上面asyncFake()改成异步执行的:

1
2
3
4
5
function asyncReal(data, callback) {
process.nextTick(function() {
callback(data === 'foo');
});
}

用在事件触发过程中

来看一个例子,你想写一个库实现这样的功能:从源文件里读取数据,当读取完毕后,触发一个事件同时传递读取的数据。可能你会这样写:

1
2
3
4
5
6
7
8
9
10
var EventEmitter = require('events').EventEmitter;

function StreamLibrary(resourceName) {
this.emit('start');

// read from the file, and for every chunk read, do:
this.emit('data', chunkRead);
}
StreamLibrary.prototype.__proto__ = EventEmitter.prototype;
// inherit from EventEmitter

下面是一段调用这个库的客户端程序,我们想在程序中监听这些事件:

1
2
3
4
5
6
7
8
9
var stream = new StreamLibrary('fooResource');

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

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

但是上面的代码中,将永远接收不到“start”事件,因为在这个库实例化的时候,“start”事件会被立刻触发执行,但此时事件的回调函数还没有准备好,所以在客户端根本无法接收到这个事件。同样,我们可以用process.nextTick()来改写事件触发的过程,下面是一个正确的版本:

1
2
3
4
5
6
7
8
9
10
function StreamLibrary(resourceName) {      
var self = this;

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

// read from the file, and for every chunk read, do:
this.emit('data', chunkRead);
}

(文章部分引用了网上内容)

文章目录
  1. 1. 异常处理
  2. 2. 在多个事件里交叉执行CPU运算密集型的任务:
  3. 3. 保持回调函数异步执行的原则
  4. 4. 用在事件触发过程中
|