如果Node.js是单线程的,那为什么server.listen()会返回呢?

9

我熟悉C++和Java中的事件驱动系统。我正在尝试学习Node.js,并遇到了有趣的行为,我希望有人能解释一下底层发生了什么。

我的程序看起来像这样:

var http = require("http");


function main(){
    // Console will print the message
    console.log('Server running at http://127.0.0.1:8080/');
    var server = http.createServer(function (request, response) {

        // Send the HTTP header
        // HTTP Status: 200 : OK
        // Content Type: text/plain
        response.writeHead(200, {'Content-Type': 'text/plain'});

        // Send the response body as "Hello World"
        response.end('Hello World\n');
    });

    server.listen(8080); //Why is this not blocking
    console.log('Main completed');

    //main loop here prevents other stuff from working
}

main();

在类似Java或C的语言中,我期望有两种情况。要么server.listen提供一个事件循环,这会导致server.listen永远不会返回。要么server.listen生成一个新线程并在新线程中运行事件循环,然后立即返回调用console.log,然后关闭程序并返回。
为了测试这个,我还添加了一个忙循环,在console.log下方,看起来像这样:
var http = require("http");


function main(){
    // Console will print the message
    console.log('Server running at http://127.0.0.1:8080/');
    var server = http.createServer(function (request, response) {

        // Send the HTTP header
        // HTTP Status: 200 : OK
        // Content Type: text/plain
        response.writeHead(200, {'Content-Type': 'text/plain'});

        // Send the response body as "Hello World"
        response.end('Hello World\n');
    });

    server.listen(8080); //Why is this not blocking
    console.log('Main completed');

    while(true){
        console.log('hi');
    }
}

main();

Server.listen会立即返回并陷入忙碌循环,这是预期的。我的浏览器无法连接到服务器,这也是预期的。

如果我删除忙碌循环并回到原始代码,一旦执行console.log('Main completed');,而不是程序退出,主线程就会跳回事件循环中。

这是如何工作的?为什么主线程在返回后会跳回服务器代码?

编辑: 我认为我们解决了事件队列不在主函数中的问题,但它在哪里?谁拥有它?主函数何时参考它运行?


有用的:http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/ - jarmod
1
我理解事件循环的一部分。我的问题涉及到主线程退出并且看起来跳回到另一个点的事实。 - nbroeking
因为 server.listen 是一个异步函数,所以你最好阅读关于 LivUV 和 EventEmmiter 的相关文档。 - The Reason
3个回答

8
http.createServer(handler)server.listen(port)看作是与浏览器中的someElement.addEventListener('click', handler)有些相似。
当你运行someElement.addEventListener('click', handler)时,它会绑定一个事件监听器,当在someElement上触发点击事件时,将把handler发送到回调队列中。 http.createServer(handler)server.listen(port)的工作方式非常类似。 http.createServer(handler)返回一个eventEmitter,而server.listen(port)告诉node.js,在给定端口接收到http请求时应该触发此事件。因此,当请求到达时,事件被触发,handler被推送到回调队列中。
此时,关键在于事件循环调用栈和回调队列的交互方式。 调用栈是当前正在执行的函数堆栈,回调队列是等待执行的回调函数集合,事件循环是从回调队列中拉取回调并将其发送到调用栈的机制。
因此,在某种意义上,事件循环是保持一切运行的机制,但是,如果您通过在其中一个回调函数中使用同步无限循环来阻止调用栈清空,则事件循环将不再运行,回调函数也将无法执行,导致应用程序崩溃/无响应。

如果您仍然不太理解事件循环/回调队列/调用堆栈,请观看此视频。https://www.youtube.com/watch?v=8aGhZQkoFbQ - Kevin B
你的回答帮了我很多。我不是在问事件循环如何工作,而是它在哪里?我原本以为它在 listen 函数内部。但现在我明白了,它并不在那里,但它仍然必须存在于某个地方。例如,在 Java 中,如果要设置事件队列,您首先需要创建一个充当事件队列的执行器,然后将事件/可运行对象推送到其中。同样的原则也适用于 Node。那个事件队列必须存在于某个地方。我只是想弄清楚它在哪里。 - nbroeking
1
据我理解(可能有误,需要进一步研究),Node.js 实现了回调队列和事件循环,而非调用栈。你无法控制它,你只能通过持续清除调用栈或阻塞它来影响它。 - Kevin B

2
这里需要理解的基本概念是事件循环和协作式(非抢占式)多任务模型server.listen 调用会向底层的事件循环注册一个回调函数,该回调函数与每个其他已注册的回调函数共享主执行线程。
当你加入那个繁忙的循环时,server.listen 回调函数永远不会得到执行时间(事实上,事件引擎也不会),因为该系统是非抢占式。也就是说,没有回调可以打断其他回调 - 回调必须通过终止来将控制权归还给事件循环,然后事件循环将根据其排队的事件调用其他已注册的回调函数。

1
主函数的结尾如何知道跳回事件循环呢? - nbroeking
我想我明白了。主函数本身就是一个处理过的事件吗?因此,当主函数完成时,它会返回到在引擎中运行的事件循环中? - nbroeking
1
不,这是一个函数。:) 你的调用栈看起来像这样:global -> main 直到 main 返回,global 才能返回。只有当调用栈为空时,事件循环才会循环。在你的代码中,main 不会返回,直到 while 循环结束... 而这永远不会发生,因此事件循环永远不会运行。 - Kevin B
main不是事件回调,但它也不是根执行上下文。 (@KevinB 知道怎么回事。) 重要的是当 main 返回时,执行循环已经准备好并在运行。 - kdbanman
我不认为我会将其视为根执行上下文,因为有些事情超出了事件循环和JavaScript运行时的范畴,比如文件系统访问和套接字,这些在完成后会将回调插入回调队列中(然后由事件循环处理),但我们正在进入一个我不太熟悉的领域,我更了解JavaScript方面的内容,包括调用堆栈、事件循环和回调队列。 - Kevin B
显示剩余5条评论

0

基本上,http.createServer 返回一个指向 EventEmitter 的指针。您可以将侦听器等附加到对象上,在事件发生时执行。这些在异步运行的事件循环中处理,不会阻塞主线程。有关 HTTPEventEmitters 的更多信息,请查看 HTTP 文档。


如果调用是异步的,那么为什么繁忙循环会阻止事件被处理? - nbroeking
2
因为繁忙的循环阻止了所有回调的处理;直到调用栈清空,事件循环才能运行。 - Kevin B

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接