我正在调查我们在生产中运行的一段表现不佳的Node.js代码,我看到了这个奇怪的小范例,故意创建一个无限循环。我希望这是一个反模式,但无论如何,当我玩它时,我发现了一些奇怪的行为。
基本思想是我们有一个192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] init
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] password
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] query
192.168.21.34 - nickh [23/Mar/2017:09:43:36 -0400] terminate
函数可以完成一些不确定的长时间工作,然后调用它的回调,以及一个outer
函数也可以做一些不确定的工作(涉及一个数据库)连接),但仅在调用inner
之后。因此,理想情况下,两个功能都会在15秒内完成,进入睡眠状态,然后重新启动以重新同步环境,但当然并不总是会发生这种情况。
由于一些原因,这似乎很明显,这是一个糟糕的设计,但无论如何,我发现以下玩具示例的行为确实令人惊讶。
setTimeout(function() {outer(inner)}, someDelay)
当我们运行它时,外部函数每秒“完成其工作”,导致每2秒向堆栈添加一个额外的'use strict';
var outer = function(callback) {
setTimeout(doWork, 1000);
function doWork() {
console.log('Did outer work.');
callback();
}
}
var inner = function() {
setTimeout(function() {outer(inner)}, 2000);
process.stdin.resume();
process.stdin.once('data', function(data) {
console.log('Got ' + data);
process.stdin.pause();
console.log('Did inner work.');
});
}
outer(inner);
调用(我想)。但是,对outer(inner)
的每次调用都会阻塞等待inner()
(这个想法是模拟数据库锁定)。如果我在向stdin
发送任何内容之前等待outer()
被调用5次,那么该堆栈上的所有5个stdin
实例将处理相同的数据。
我真的很惊讶 - 我想我只希望一个函数调用接受第一个输入inner()
而另外四个要阻止。同样,这是一个糟糕的设计,但是 - 问题:这是可取的吗?有人故意让stdin
和JS callstack以这种方式行事,还是其他语言功能的副作用?
或者这是否有意义,我只是以错误的方式思考它?
提前致谢,我很抱歉这是一个模糊的。
答案 0 :(得分:2)
我希望它是一种反模式
不,它没有错。大多数类似服务器的应用程序使用无限循环进行编码 - 您希望它们永远处理连接(即,直到您杀死它或崩溃)。
导致每2秒向堆栈添加一个额外的外部(内部)调用(我想)。
没有。 setTimeout
是异步的,这意味着它立即返回,其余代码运行完成,然后在新的新调用堆栈上执行预定的回调。堆栈不会以这种半递归模式增长。
但是,对inner()的每次调用都会在等待stdin时阻塞(这个想法是模拟数据库锁)。
不,once()
不会阻止。它只安装一个处理程序,该事件将在下次发生事件时执行一次。
如果我等到在向stdin发送任何内容之前调用outer()5次之后,我真的很惊讶,相同的数据由堆栈上的所有5个inner()实例处理。
是的 - 有5个data
事件处理程序在等待事件,并且在事件被触发时它们都会被输入执行。
如果你想在继续递归之前阻塞直到事件发生,你需要将setTimeout
安排在data
事件监听器中安排下一次读取:
function inner() {
process.stdin.resume();
process.stdin.once('data', function(data) {
console.log('Got ' + data);
process.stdin.pause();
console.log('Did inner work.');
setTimeout(function() {outer(inner)}, 2000);
});
}
您甚至可以在此省略超时并直接致电outer(inner);
。
答案 1 :(得分:1)
行为并不令人惊讶:您正在将多个单独的处理程序连接到stdin
data
事件,因此当事件运行时,它会调用每个处理程序。它完全是这样的:
process.stdin.once('data', function(data) {
console.log("callback1: " + data);
});
process.stdin.once('data', function(data) {
console.log("callback2: " + data);
});
process.stdin.once('data', function(data) {
console.log("callback3: " + data);
});
如果你运行它,然后键入内容并按Enter键,所有三个回调都会收到该事件,因为毕竟所有三个回调都是为该事件注册的。
这正是inner
正在做的事情:挂钩data
事件以进行多次回调。
这个版本显示了挂钩到data
事件的处理程序的数量(当事情实际排队时),可能会让事情变得更清晰:
'use strict';
var handlerCounter = 0;
var outer = function(callback) {
console.log("outer called, queue doWork for 1s from now");
setTimeout(doWork, 1000);
function doWork() {
console.log('doWork called');
callback();
}
}
var inner = function() {
console.log('Queueing outer(inner) call for 2s from now');
setTimeout(outer, 2000, inner);
process.stdin.resume();
++handlerCounter;
console.log('Adding another handler to the `data` event, total will be: ' + handlerCounter);
process.stdin.once('data', function(data) {
--handlerCounter; // Since `once` removed it
console.log('Got ' + data + ', handlers now: ' + handlerCounter);
process.stdin.pause();
});
}
outer(inner);