Day 14 - Asynchronous 非同步核心

前言

非同步一直是困扰着 Javascript 新手的小魔王,以前常常会有「为什麽这行先跑到没有先执行?」这种困扰。

随着时间过去,踩了几次 bug,也渐渐抓到非同步的节奏,但在这个阶段,往往也只是知道,非同步就是不断地等:

  • setTimeout 就是等几秒就会执行
  • addEventListener 就是要等事件触发
  • fetch 就是要等後端回应

我们知道非同步「怎麽用」,却不一定知道「为什麽」,让我们来看看非同步的核心究竟是怎麽运作的。

Javascript Runtime

首先需要提到的是 Javascript Runtime,中文大概可以翻成 Javascript 的「执行环境」吧,比如 Chrome、Firefox、node,每个 runtime 提供的 API 都不同,所以不是所有地方都有 window 物件,setTimeout 之类的 Web API。

Runtime 会随着环境而不同,但有两个机制,是属於 Javascript 的机制,因此任何地方都一样:Call Stack(存放指令)、Memory Heap(存放资料)

另外还有两个名词也先介绍一下:

  • Callback Queue:用来存放从 Web api 过来,准备要进入 Call Stack 的指令
  • Event Loop:会不断监看 Call Stack,如果空了就会把 Callback Queue 的指令放到 Call Stack 执行

程序码在背景的处理顺序

对於一段程序码,Javascript engine 底层会依序做这些事:

  1. 把 JS 的指令一行一行放到 Call Stack,并且执行
  2. 途中如果遇到不属於 JS 自身 (如: Web API) 的指令,因为 JS 看不懂,会交由 Web API 处理
  3. Web API 处理後的(如: setTimeout 秒数数完)程序码会放到 Callback Queue
  4. Event Loop 不断地监看 Call Stack 是否空了,如果空了就会把 Callback Queue 的指令放到 Call Stack 执行

范例

以下程序码为例,用到 $.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");
  1. $.on() 放到 Call Stack
  2. $.on() 交由 Web API 处理(因为不是原生 JS)
  3. Web API 开始等待按钮点击事件
  4. console.log("Hi!") 放到 Call Stack,执行
  5. setTimeout() 放到 Call Stack
  6. setTimeout() 交由 Web API 处理(因为不是原生 JS)
  7. Web API 开始等待 5 秒
  8. console.log("Welcome") 放到 Call Stack,执行

至此,Call Stack 已净空,Event Loop 会把 Callback Queue 里面的指令搬到 Call Stack 执行

  1. (过了 5 秒)
  2. Web API 内的 setTimeout() 的 callback function 被搬到 Callback Queue
  3. Event Loop 把 setTimeout() 的 callback function 搬到 Call Stack,执行

至此,Call Stack 再度净空

  1. (使用者点击了按钮)
  2. Web API 内的 $.on() 的 callback function 被搬到 Callback Queue
  3. Event Loop 把 $.on() 的 callback function 搬到 Call Stack,执行
  4. setTimeout() 交由 Web API 处理(因为不是原生 JS)
  5. Web API 开始等待 2 秒

至此,Call Stack 再度净空

  1. (过了 2 秒)
  2. Web API 内的 setTimeout() 的 callback function 被搬到 Callback Queue
  3. Event Loop 把 setTimeout() 的 callback function 搬到 Call Stack,执行

至此,Call Stack 再度净空

如果我提早点击按钮?

上述的流程是比较顺的正向流程,但真实情境下,哪会在那边等 5 秒才按按钮啊,如果我们提早点击按钮,会发生什麽事?

你会发现,因为你点击,Callback Queue 很早就有指令了,但那个指令只能乖乖排队,等程序码跑完最後一行程序,才会轮到它,因为 Event Loop 要等 Call Stack 的指令都跑完,才会放 Callback Queue 的人进来。

setTimeout 0 秒算是同步还非同步?

console.log('1');
setTimeout(() => {
    console.log('2');
}, 0);
console.log('3');

这问题真的很妙,「我等了你 0 秒,请问我算是有等你吗?」,彷佛成了哲学思考问题XD

如果 setTimeout 只用一句很简单的「等了几秒就执行」来概括,就会在这个范例被卡住,因为这个范例一秒都不用等,那是不是就会立刻执行呢?

大家可以试着自己想想上述的程序码会印出什麽,其实核心问题跟上一题提早点击是一样的。

最後答案是:

1
3
2

没错,只要是非同步(如 Web Api)的程序码,就一定要进 callback queue 蹲着,不管有多快进去,都一定要等 call stack 的程序码都跑完,才有可能轮到它。

对於新手比较容易理解的会是这样:「要等同步都执行完,才会轮到非同步」。

视觉化的 Playground

这个网站是我极力推荐的地方,我的抽象思维不太好,没办法在脑袋中把同步跟非同步搅在一起,可以透过这个 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资料库,存储订单交易资讯

>>:  [DAY 16] _Si7020温湿度读写

Day-25: Ruby 世界好多等於,系虾米毁?

今天来说明一下,在Ruby的世界里,运算符代表什麽意思? 之前偶然间在等候区,和同学们讨论这个问题,...

人脸辨识-day16 应用层面--2

人脸辨识还应用在只让特定人员才可进行存取或使用。如以下的例子: 1.在公司行号利用人脸辨识系统自动化...

Day14 NiFi - NiFi Expression Language

今天要来介绍的是 NiFi Expression Language (以下简称NEL)。在前一篇我们...

30天学会 Python: Day 12-人生苦短,使用 Python

Python 还有很多不同功能的内建函式,以下列出一些满常用到的 数学相关 abs(x) 回传 x ...

Day 25 - Vue CLI 与 npm

在进行开发的时候,我们常常会下载各式各样的library,如果我们没有一个工具来帮助我们维护这些套件...