问题描述
有人可以解释一下 javascript 异步行为背后的机制,特别是当涉及 .subscribe() 时,或者提供一个链接来帮助我了解幕后真正发生的事情吗?
一切似乎都是单线程的,然后我遇到了使其成为多线程或看起来是多线程的情况,因为我仍然无法摆脱这种被愚弄的感觉。
一切似乎都完美地同步运行,一次一条指令,直到我调用 .subscribe()。然后,突然发生了一些事情——它立即返回,并且不知何故下一个外部函数运行,即使订阅代码还没有。这对我来说感觉是异步的,浏览器如何让事情保持直截了当 - 如果不是线程,有人可以解释一下允许这种情况发生的机制吗?
我还注意到调试单步执行时发生了一些事情,我无法进入回调,我只能在其中放置一个断点。我真的应该期待事情发生的顺序是什么?
解决方法
除非您谈论的是 WebWorkers(在浏览器中)或 WorkerThreads(在 nodejs 中),否则您的 Javascript 本身运行单线程。
但是,您在 Javascript 中调用的许多函数或启动的操作可以与 Javascript 的单线程性质分开运行。例如,网络都是非阻塞和异步的。这意味着当您在 nodejs 中的 fetch()
的浏览器中调用 http.get()
时,您正在发起一个 http 请求,然后发出该请求和接收响应的机制独立于您的 Javascript .以下是一些示例步骤:
- 您在浏览器中调用
fetch()
以向其他主机发出 http 请求。 - fetch 背后的代码发出 http 请求,然后立即返回。
- 您的 Javascript 将继续执行(您在
fetch()
调用后立即执行的代码行中的任何内容都将执行。 - 稍后,来自
fetch()
调用的响应返回到网络接口。一些代码(在 Javascript 环境内部)并且独立于您的单线程 Javascript 看到有传入数据要从 http 响应中读取。当该代码收集到 http 响应时,它会将一个事件插入到 Javascript 事件队列中。 - 当 Javascript 的单线程执行完它正在执行的任何事件时,它会检查事件队列以查看是否还有其他事情要做。在某个时候,它会找到表示前一个
fetch()
调用完成的事件,并且会有一个与该事件关联的回调函数。它将调用该回调,该回调将解决与fetch()
调用相关联的承诺,这将导致.then()
处理程序中您自己的 Javascript 运行该承诺,并且您的 Javascript 将收到您正在等待的 http 响应
因为这些非阻塞、异步操作(例如网络、计时器、磁盘 I/O 等)都以相同的方式工作,并且可以独立于您的 Javascript 执行进行,它们可以与您的Javascript 执行和其中许多可以在您的 Javascript 执行其他操作的同时进行。这有时可能会出现多个线程,并且可能有一些本机代码线程参与其中的一些,尽管您的 Javascript 仍然只有一个线程(只要我们不是在谈论 WebWorkers 或 WorkerThreads)。>
这些异步操作的完成然后通过事件队列与Javascript的单线程同步。当它们完成时,它们将一个事件放入事件队列中,当 Javascript 的单个线程完成它上次执行的操作时,它会从事件队列中获取下一个事件并对其进行处理(调用 Javascript 回调)。
我还注意到调试单步执行时发生了一些事情,我无法进入回调,我只能在其中放置一个断点。我真的应该期待事情发生的顺序是什么?
这是因为回调不是同步调用的。当您跨过传递回调的函数时,它会执行该函数并返回。您的回调尚未调用。因此,调试器完全按照您的要求执行。它执行该函数并返回该函数。回调尚未执行。你是对的,如果你想看到异步回调执行,你需要在其中放置一个断点,这样当它在未来的某个时间被调用时,调试器会在那里停下来让你更进一步。这是调试异步事物的复杂性,但过一段时间你就习惯了。
一切似乎都是单线程的,然后我遇到了使其成为多线程或看起来是多线程的情况,因为我仍然无法摆脱这种被愚弄的感觉。
这是因为可能有许多异步操作同时进行,因为它们在执行任务时没有运行您的 Javascript。因此,尽管有多个异步操作在进行,但仍然只有一个 Javascript 线程。这是一个代码片段:
console.log("starting...");
fetch(host1).then(result1 => { console.log(result1)});
fetch(host2).then(result2 => { console.log(result2)});
fetch(host3).then(result3 => { console.log(result3)});
fetch(host4).then(result4 => { console.log(result4)});
console.log("everything running...");
如果你运行这个,你会在控制台看到类似这样的东西:
starting...
everything running...
result1
result2
result3
result4
实际上,这 4 个结果之间的顺序可能是任意的,但它们将在调试输出的前两行之后。在本例中,您启动了四个单独的 fetch()
操作,它们独立于您的 Javascript 运行。事实上,您的 Javascript 在运行时可以自由地做其他事情。当它们完成并且您的 Javascript 没有执行任何其他操作时,您的四个 .then()
处理程序将在适当的时间分别被调用。
当每个 fetch()
操作背后的网络完成时,它会在 Javascript 事件队列中插入一个事件,如果您的 Javascript 当时没有做任何事情,该事件的插入将唤醒它并导致它来处理那个完成事件。如果它当时正在做某事,那么当它完成那个特定的 Javascript 并且控制返回给系统时,它会看到有一个事件在事件队列中等待并处理它,调用与该事件关联的 Javascript 回调.
搜索有关“JavaScript 事件循环”的文章或视频。你会发现很多。可能通过其中的一些,它就会开始变得有意义(尤其是它的单线程方面)
我通过快速搜索找到了这个,它提供了一个很好的、非常高级的演练。
The JavaScript Event Loop explained
JavaScript 单线程的主要影响是您的代码永远不会被抢先中断以在另一个线程上执行代码。
看起来有些代码被跳过了,但仔细观察语法会发现被“跳过”的只是一个回调函数或闭包,在未来某个时间被调用(当它被放置时)进入事件队列并单线程处理,本身)。
小心,跟踪嵌套回调可能会很棘手。
可能还会研究 Promises 和 async /await(‘只是事件循环的语法便利,但确实有助于代码可读性)