非同步一直是困扰着 Javascript 新手的小魔王,以前常常会有「为什麽这行先跑到没有先执行?」这种困扰。
随着时间过去,踩了几次 bug,也渐渐抓到非同步的节奏,但在这个阶段,往往也只是知道,非同步就是不断地等:
setTimeout
就是等几秒就会执行addEventListener
就是要等事件触发fetch
就是要等後端回应我们知道非同步「怎麽用」,却不一定知道「为什麽」,让我们来看看非同步的核心究竟是怎麽运作的。
首先需要提到的是 Javascript Runtime,中文大概可以翻成 Javascript 的「执行环境」吧,比如 Chrome、Firefox、node,每个 runtime 提供的 API 都不同,所以不是所有地方都有 window
物件,setTimeout
之类的 Web API。
Runtime 会随着环境而不同,但有两个机制,是属於 Javascript 的机制,因此任何地方都一样:Call Stack(存放指令)、Memory Heap(存放资料)
另外还有两个名词也先介绍一下:
对於一段程序码,Javascript engine 底层会依序做这些事:
setTimeout
秒数数完)程序码会放到 Callback Queue以下程序码为例,用到 $.on
(类似 addEventListener
) 跟 setTimeout
这种非同步的程序码,但中间也夹杂了一些 console
,在 background 会怎麽运作呢?
$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('clicked');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome");
$.on()
放到 Call Stack$.on()
交由 Web API 处理(因为不是原生 JS)console.log("Hi!")
放到 Call Stack,执行setTimeout()
放到 Call StacksetTimeout()
交由 Web API 处理(因为不是原生 JS)console.log("Welcome")
放到 Call Stack,执行至此,Call Stack 已净空,Event Loop 会把 Callback Queue 里面的指令搬到 Call Stack 执行
setTimeout()
的 callback function 被搬到 Callback QueuesetTimeout()
的 callback function 搬到 Call Stack,执行至此,Call Stack 再度净空
$.on()
的 callback function 被搬到 Callback Queue$.on()
的 callback function 搬到 Call Stack,执行setTimeout()
交由 Web API 处理(因为不是原生 JS)至此,Call Stack 再度净空
setTimeout()
的 callback function 被搬到 Callback QueuesetTimeout()
的 callback function 搬到 Call Stack,执行至此,Call Stack 再度净空
上述的流程是比较顺的正向流程,但真实情境下,哪会在那边等 5 秒才按按钮啊,如果我们提早点击按钮,会发生什麽事?
你会发现,因为你点击,Callback Queue 很早就有指令了,但那个指令只能乖乖排队,等程序码跑完最後一行程序,才会轮到它,因为 Event Loop 要等 Call Stack 的指令都跑完,才会放 Callback Queue 的人进来。
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
这问题真的很妙,「我等了你 0 秒,请问我算是有等你吗?」,彷佛成了哲学思考问题XD
如果 setTimeout
只用一句很简单的「等了几秒就执行」来概括,就会在这个范例被卡住,因为这个范例一秒都不用等,那是不是就会立刻执行呢?
大家可以试着自己想想上述的程序码会印出什麽,其实核心问题跟上一题提早点击是一样的。
最後答案是:
1
3
2
没错,只要是非同步(如 Web Api)的程序码,就一定要进 callback queue 蹲着,不管有多快进去,都一定要等 call stack 的程序码都跑完,才有可能轮到它。
对於新手比较容易理解的会是这样:「要等同步都执行完,才会轮到非同步」。
这个网站是我极力推荐的地方,我的抽象思维不太好,没办法在脑袋中把同步跟非同步搅在一起,可以透过这个 playground,把你写的 code 实际在 background 执行起来,连 background 在做的事情都清楚显示给你看,特别适合像我一样的视觉化动物(?)。
可以看到当你将上面的程序贴上去,按下 Save and Run,程序并不会咻一声就跑完,而是用大约每一秒 2 个指令的速度,把这行程序码是被放到 Call Stack 还是 Callback Queue,清楚显示在画面上,可以很清楚知道电脑现在正在处理哪个指令。如果还是嫌太快,作者也准备了 Pause 按钮,按照自己的步调调整。
前面两题关於「提早点击」与「setTimeout 0 秒」的问题,你都可以在这个 playground 找到解答。
如果你很有耐心,把上述一大串都看完,就会知道「非同步」诞生的原因,真的不是凭空诞生的,而是因为就像上面的 Web Api 那样,对於 Javascript 以外的指令,透过背景运作的机制来完成,就是所谓的「非同步」。
看到这里,下次被问到「setTimeout 为什麽是非同步?」的时候,你会怎麽回答呢?笔者曾经只能回答「因为要等」这种答案XD,现在才知道背後的学问可大了呢!
另外也有人会疑惑说:「我懂同步跟非同步的意思了,但我不明白,同步两个字听起来像是大家一起、同时的感觉,但是为什麽在 Javascript 却是代表一个接一个、陆续的感觉?」
这比较是视角不同的缘故,或许可以改用这种方式重新理解:
非同步难归难,背後的世界却非常广阔,当我们真的学会了 Javascript 是如何处理非同步程序码,才更能在每一次程序运作不如预期时,一步步推敲出问题核心,就离更好的 developer 更近了一步!
在一个世界线
交织着两个平行宇宙
踏着不同的步调
写着同一个故事
the-call-stack-and-memory-heap
<<: Day14 - [丰收款] 使用Heroku Postgres资料库,存储订单交易资讯
今天来说明一下,在Ruby的世界里,运算符代表什麽意思? 之前偶然间在等候区,和同学们讨论这个问题,...
人脸辨识还应用在只让特定人员才可进行存取或使用。如以下的例子: 1.在公司行号利用人脸辨识系统自动化...
今天要来介绍的是 NiFi Expression Language (以下简称NEL)。在前一篇我们...
Python 还有很多不同功能的内建函式,以下列出一些满常用到的 数学相关 abs(x) 回传 x ...
在进行开发的时候,我们常常会下载各式各样的library,如果我们没有一个工具来帮助我们维护这些套件...