7. 解释 Event Loop ( 上 ) --- Call Stack

9.9更新: 更正呼叫堆叠的内部为 stack frame。

(提醒:文中的执行环境都是browser(chrome))

今天要解释JS的Event Loop事件循环,分成上下两篇,这篇还不会讲到Event Loop本身!!

上篇:

  • Call Stack
  • 一点点 Synchronous & Asynchronous 的概念

下篇:

  • Web APIs & Task Queue
  • Event Loop

Call Stack 呼叫堆叠


在之前的文章 JavaScript的执行阶段: Execution Context 有提到,在execution phase里(绿色框框),都是依照 Call Stack的方式执行script。

https://ithelp.ithome.com.tw/upload/images/20210908/20129476ePVRCd5fOV.png

https://ithelp.ithome.com.tw/upload/images/20210908/20129476tFu4W31oif.png
↑execution phase里(绿色框框)

这边要先提到Stack的概念:

Stack 堆叠

资料结构是指电脑储存资料的方式,分成很多种,而「Stack(堆叠)」是一种资料结构。
Stack的特色是 後进先出(Last-in-First-out, LIFO)。

可以想像Stack是一种由下而上堆叠起来的frame(Function calls from a stack of ***frames.***):
https://ithelp.ithome.com.tw/upload/images/20210908/20129476lnHUY79kUD.png

  • 最下面是第一个被放入的frame,然後frame被一个一个堆起来。→ push(黄色箭头)
  • 如果要把frame抽走,只能从最上方开始拿。→ pop(绿色箭头)
  • 因此可以知道,越下方是越早放入的frame,越上方则是越晚放入的frame(越新)。
  • 因为最晚放入的,会被最早拿出,所以被称为"後进先出(Last-in-First-out, LIFO)。"

到这边应该对Stack比较有概念了,接下来要开始说明Call Stack。

Call Stack


让我们先看到例子:

function a(){
    function b(){
        function c(){
            console.log('This is c()');
        }
        c();
        console.log('This is b()');
    }
    b();
    console.log('This is a()');
}
a();

然後他的console会是这样:

This is c()
This is b()
This is a()

在execution phase里,决定事件处理的顺序是这麽做的:

  1. 呼叫a(): 将a放入call stack,
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476wasBnEe0HC.png

  2. 开始读取function a()里的程序码,读到b(),b也放入call stack。
    https://ithelp.ithome.com.tw/upload/images/20210908/201294765LCNAxvDEj.png

  3. 开始读取function b()里的程序码,读到c(),c也放入call stack。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476N5o71DNIcf.png

  4. 执行console.log('This is c()');;c()执行完成,从call stack移除c。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476NeCqlWUQur.png

  5. 执行console.log('This is b()');;b()执行完成,从call stack移除b。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476iAqcUPTKDr.png

  6. 执行console.log('This is a()');;a()执行完成,从call stack移除a。
    https://ithelp.ithome.com.tw/upload/images/20210908/20129476gyw7PJSElJ.png

  7. a()执行完毕,call stack被清空。
    https://ithelp.ithome.com.tw/upload/images/20210908/201294767RxqVUDwgP.png

→ 想看动态的可以看这里,gif版(imgur)

→ 也可以把程序码丢到这看效果(非常之厉害的视觉化工具)【loupe】

到这边划个重点:

Call Stack是在 函式里呼叫函式 时的一种执行机制。

  • 每调用一次函式,函式都会被放进Call Stack。

  • 只要正在执行的函式里调用了新的函式,新函式也会被放入Call Stack。

  • 函式执行完成,会被清出,直到call stack被清空。

再让我们看一个例子:

function f(){
    f();
}

f();

可想而知,执行f()时,call stack可能会长这样:

https://ithelp.ithome.com.tw/upload/images/20210908/20129476vgjnXkdDxM.png

这种超过Call Stack容量的情况,称作Stack overflow。

很遗憾的,这个函式会呼叫自己一辈子,这代表我们永远看不到这个函式执行完的结果。还有电脑可能被烧坏

如果想要解决这个问题,最常见的方法就是使用非同步函式 Asynchronous function。

同步与非同步


Synchronous 同步

当跑一段程序码(script),浏览器(bowser)会将执行结果直接回传,这样的执行方式叫做"同步"。

(包含文内的例子(还有至今为止的文章),执行方式都是同步的。)

由於JS是单执行绪(thread),程序码的执行顺序当下同一时间只会处理一件事。所以只要有一段程序码还没有执行完成,其他的任务就都会停摆,这样会造成时间上的浪费。

JavaScript 在传统意义上是跑在一条单执行绪。即便你的电脑有多颗核心,也只能在 JavaScript 上面跑一条执行绪来完成任务,这一条执行绪我们称为主执行绪( main thread )。

为了解决这个问题,便有了非同步程序设计。

Asynchronous 非同步

非同步(Asynchronous,又称异步)则与同步相反,在指定时间或情形,才处理和接收讯息,而非在读到程序码时直接执行。

可以看到MDN对非同步的介绍:

Javascript 基本上是一个同步性的、阻塞的,且是跑在单一执行绪的程序语言,也就是在同一时间只能执行一个操作。但是 浏览器所定义的函式和 API 允许我们注册一个不该被同步执行的函式,且这个函式应该在某些事件发生时需要非同步的被呼叫 (到达指定的时间、使用者透过滑鼠互动,或者取得透过从网路所取到的资料)。这代表你可以让你的程序码在同时间做一些事情而不需暂停或阻塞你的主执行绪。

如果能将其他任务交给其他处理器来执行,并确认任务何时完成,就可以缓解浪费时间、效率不佳的问题。

那麽下一篇就会讲到非同步函式的执行,然後就可以完整解释Event Loop了。

【如内文有误还请不吝指教>< 谢谢阅览至此的各位:D】

参考资料:

-----正文结束-----


<<:  Amazon Linux 2 上将 Django 与 Nginx 整合 -Day 08

>>:  离职倒数23天:东京女子图监

DAY6 - 挑选一套自己喜欢的UI框架

搞定了架构和想法後,再来就要搞定画面的呈现。自己刻画面固然可以随心所欲呈现自己想要的样子与控制自己只...

Day 30 - 开发流程(下) Web 开发流程 & 铁人赛心得

上一篇Day 29 - 开发流程(上) 瀑布式(Waterfall Model) & 敏捷式...

ESP32_DAY5 来新建一个专案吧!

程序架构 昨天只有介绍到Arduino的程序架构可以分为两大函式: setup()及loop()。 ...

Day 29:30 天自我挑战阅读说明

一开始就定调这挑战是个流水帐,所以写的东西也随性许多,最後两天,先将阅读顺序整理一下,至於最後一天会...

D13: 工程师太师了: 第7话

工程师太师了: 第7话 杂记: 这次的COVID-19 疫情, 究竟对我们产生了什麽影响? 很多人会...