为什么JavaScript是单线程和非阻塞呢?

373 阅读3分钟

JavaScript,单线程但不阻塞

当人们说JavaScript是一种单线程和非阻塞的编程语言时,那些刚刚接触到JavaScript的人可能会感到困惑。你可能会想,怎么可能是单线程而非阻塞的呢?

单线程

JavaScript之所以被称为单线程,是因为它只有一个调用栈,而其他一些编程语言则有多个调用栈。JavaScript的函数是在调用堆栈上执行的,通过LIFO(Last In First Out)。例如,我们有一段这样的代码。

const foo = () => {  const bar = () => {    console.trace();  }  bar();}

而调用栈中会有foo进入调用栈,然后是bar。

在bar()完成后,它将从调用栈中弹出,接着是foo()。在打印出堆栈跟踪时,你会看到下面有一个匿名函数,这是主线程的全局执行上下文。

这似乎是合乎逻辑的,因为JavaScript是一种单线程语言,只有一个流程来执行所有这些函数。然而,如果我们在流程中有一些不可预测的或繁重的任务(例如进行API调用),我们不希望它们阻塞其余代码的执行(否则用户可能会盯着一个冻结的屏幕)。这就是异步JavaScript的用处。

无阻塞

除了JavaScript引擎之外,我们还有Web APIs、回调队列和事件循环来形成浏览器中的JavaScript运行时间。比方说,我们这里有一段代码。

console.log("1")setTimeout(() => console.log("2"), 5000)console.log("3")

"setTimeout "是一个Web API函数,它将在一定时间后执行一个回调函数(单位是毫秒,在这里是5000毫秒)。当你执行这个脚本时,你会看到 "1 "和 "3 "被立即打印出来,而 "2 "则在5秒左右被打印出来。

这就是幕后发生的事情。

第一个控制台日志被放入堆栈,在控制台中打印出 "1 "后被跳出。当setTimeout函数被放进堆栈时,回调函数被这个Web API函数设置为等待。然后setTimeout函数被弹出堆栈,第三个控制台日志进入。执行完毕后,第三个控制台日志和当前的全局执行上下文被从堆栈中弹出。

当setTimeout中的回调函数完成等待时,它将进入回调队列(或事件队列)并等待被执行。事件循环促进并检查调用栈是否为空。如果是空的,就会创建一个新的全局执行上下文,这个回调函数(console log out "2")就会被放入堆栈,执行,然后弹出。

补充一点,即使你把setTimeout设置为延迟0秒,"2 "仍然会是最后一个被打印出来的,因为只要Web API被调用,它就会被放入回调队列,只有当堆栈为空时才会被放入堆栈。

我希望这能让你了解为什么JavaScript可以同时实现单线程和非阻塞。

哦,对了,如果你还需要一个视频解释,这里有一个很好的资源。

事件循环到底是什么?| Philip Roberts | JSConf EU

如果你想看到更多的网络开发或软件工程相关内容,请关注我。欢呼吧!