令人惊讶的是,不!我相信您的代码应该可以正常运行。
JavaScript 中的异步操作,例如 setTimeout()、XMLHttpRequest 和 Promise 利用称为 Event Loop 的东西在程序的正常流程之外执行。实际上,当异步发生时,它会被添加到“队列”(与堆栈完全分离)。一旦堆栈清空,事件循环将开始处理这些排队的消息,并一一执行它们相关的功能。
JavaScript 中的几乎所有异步都以这种方式工作,包括 Q Promise 库。因此,在您的示例中,这是对正在发生的事情的(非常)简化的解释:
-
main() 被调用,创建一个新的堆栈帧。
-
Q.fcall(somePromiseFunction) 被调用,创建第二个堆栈帧(大概是第三个,当somePromiseFunction 被调用时。这会在后台触发异步操作,将传递给then 的函数设置为回调。
-
Q.fcall(somePromiseFunction) 现在已返回,因此这些堆栈帧已被清除。
-
main() 也到了末尾,所以它也被清除了 - 我们回到了一个空堆栈。
- 异步操作完成后,会向事件队列推送一条消息,并与您的
then 回调关联。
- 堆栈为空,因此事件循环开始处理。
- 您的回调被执行。
这里要注意的重要一点是您的代码不会递归!您的 then 回调只会从事件队列中调用,而不是由 main() 调用,因此堆栈能够在迭代之间毫无问题地清除。
我建议观看'What the heck is the event loop anyway?' 的演讲,以了解有关这一切如何运作的更多信息 - 他们解释它的方式是它最终吸引我的原因。
编辑:
为了回答您在 cmets 中的问题,我将澄清一些事情。最简单的递归函数如下所示:
function recursive() {
recursive();
}
调用一个函数会分配一个栈帧,当它返回时会被清除。然而,如果没有某种方式打破循环,像这个这样的递归函数永远不会返回 - 它会继续运行,无休止地分配堆栈帧,直到出现堆栈溢出错误。这并不是说你不应该在 JavaScript 中使用递归——它有时会非常有用!但是你需要注意,如果它递归太多而没有返回,你就会碰到栈顶。
回答您的其他问题 - 事件循环/您的回调不会等待 main() 特别结束,它只是等待堆栈清除。以下是堆栈在您的程序中的作用的简化可视化:
[]
[main]
[main, fcall]
[main, fcall, somePromiseFunction]
[main, fcall]
[main]
[main, then]
[main]
[] // Main returned, so the stack is clear
[yourCallback] // The event loop kicks in, runs your callback
[yourCallback, main]
... // Above steps repeat
[yourCallback] // Main returned
[] // Stack clears, so the event loop kicks in again
将此与我不断重复的功能进行比较:
[]
[recursive]
[recursive, recursive] // Nothing stops the recursion,
[recursive, recursive, recursive] // so the stack is never going to clear!