为什么exception堆栈中的根条目根据上下文而有所不同?

我的理解是(非集群)nodejs程序中的所有代码都运行在同一个线程中。 鉴于此,我期望所有这样的代码将作为同一根事件循环的子代运行,因此,如果检查了在不同callback中运行的代码的堆栈跟踪,我们仍然会最终回溯到相同的条目该事件循环的“调度事件”行)。 但事实并非如此,我不明白为什么。

考虑以下:

function printStackTrace() { console.log(new Error().stack); } printStackTrace(); setTimeout(printStackTrace, 1000); 

运行得出:

 Error at printStackTrace (/tmp/node/test.js:4:17) at Object.<anonymous> (/tmp/node/test.js:7:1) at Module._compile (module.js:446:26) at Object..js (module.js:464:10) at Module.load (module.js:353:32) at Function._load (module.js:311:12) at Array.0 (module.js:484:10) at EventEmitter._tickCallback (node.js:190:39) Error at Object.printStackTrace [as _onTimeout] (/tmp/node/test.js:4:17) at Timer.ontimeout (timers.js:94:19) 

并简单地运行console.log(new Error().stack); 在REPL中给出了一个不同的根源:

 Error at repl:1:13 at REPLServer.eval (repl.js:80:21) at repl.js:190:20 at REPLServer.eval (repl.js:87:5) at Interface.<anonymous> (repl.js:182:12) at Interface.emit (events.js:67:17) at Interface._onLine (readline.js:162:10) at Interface._line (readline.js:426:8) at Interface._ttyWrite (readline.js:603:14) at ReadStream.<anonymous> (readline.js:82:12) 

所以每个根(最底部)项是不同的(分别在EventEmitter,Timer和ReadStream中)。 其他callback(例如networking)也是如此。

所以我想也是

  • 事件循环是本地(C ++)代码,所以它不会显示在堆栈跟踪中,并且asynchronous服务的基本提供者(repl.js,timers.js等)使用本机v8 api调用。
  • 事件循环是JavaScript,但是Error()有特殊的代码来隐藏这个(作为不必要的实现细节)

哪个(如果有的话)是这种情况,一般来说,在nodejs(edit:or v8)源代码中,我可以读取真正的root事件循环的实现吗?

答案(或者至less是线索)就在堆栈跟踪中。 只需按照堆栈底部的文件中的代码。

我不确定你使用的是什么版本的节点(0.6的时间更新!),但在最新的(0.10.17),

 setTimeout(function() { console.log(new Error().stack) }, 1); 

打印出来:

 Error at null._onTimeout (repl:1:38) at Timer.listOnTimeout [as ontimeout] (timers.js:110:15) 

所以我们来看看timers.js:110 。 该行位于listOnTimeout函数中,该函数被分配给Timer实例的ontimeout属性。

Timer是一个C ++模块 ,与libuv进行交互; 它是调用 ontimeout函数的C ++代码。

所以你的答案是:堆栈的根目录是被C ++代码调用的JavaScript函数(无论是定时器还是streampipe道)。

Error提供给您的堆栈跟踪不会显示任何涉及调用函数的本机代码。 事件循环本身是由V8(本地代码)实现的,而不是JavaScript,所以它应该是有道理的,你没有看到任何超出边界的东西。

所以发生的事情非常接近你的第一个猜测。 JavaScript代码通过将某些属性设置为一个函数(或将函数作为parameter passing本机代码) 注册callback函数。 当C ++想要调用这个函数时,它会得到这个引用并指示V8通过调用v8::Function::Call来调用这个函数。

如果您对V8的工作方式感兴趣,那么embedded式指南是一个好的开始。