【第2268期】Javascript 非同步& Event Loop!10 分钟轻松图解学习!
前言
有些词需要大家稍微转换一下。今日前端早读课由@Jc授权分享。
@Jc,设计转职程式工程师。chan-chan-dev是结合设计图文与说故事比喻的能力来讲解网路技术的知识网站,希望在自己学习的路上,也能帮助更多像自己一样非理工科背景的人能更简单地学习网路技术的内容。
正文从这开始~~
有听过Javascript 是个『非同步』的语言吗?为什么它可以做到『非同步』的效果呢?一起用图解的方式来学习Javascript 的『同步』与『非同步』吧!
在学习Javascript过程中,应该多少都会耳闻过Javascript非同步的特性,在聊聊非同步的特性之前,也许我们要先知道何为同步啰😆。
同步(Sync)
我们用小明早上起床出门上班前的小例子来学习这两者的差别吧😀
假如小明从起床到上班出门前他需要完成以下几件事情:
刷牙、洗脸(10 分钟)
用义式咖啡机泡咖啡(10 分钟)
用电锅准备午餐(10 分钟)
若今天小明一件事情完成後,在繼續執行下一件事情。
例如:刷牙、洗臉 (10 分鐘)完接着用用義式咖啡機泡咖啡(10 分鐘),然后用電鍋準備午餐(10 分鐘),最后出门。我们可以说小明上述的步骤为同步(Sync)的执行每一件事情,所以总共所花的时间是30分钟。
上述的同步不等于同時,一开始接触到学习的时候很容易将同步与同時划上等号,这边要特别注意是不一样的意思喔😀
同步:一件事情完成后,在继续执行下一件事情
Javascript 同步
console .log( 'a' );
console .log( 'b' );
console .log( 'c' );
在『一般』的情况下,Javascript是以同步的方式执行程式码,因此上述的程式码会依序地一個執行完之後在執行下一個:
执行第1 行,在console 呈现 a
执行第3 行,在console 呈现 b
执行第5 行,在console 呈现 c
因此在console 会呈现:
10 // a
20 // b
30 // c
那至于什么是『非同步』呢?让我们看下去啰😆
非同步(Async)
每次出门的都要花上30分钟也没有不好,只是小明最近刚好看到一本时间管理的书,教你如何有效率的利用时间!其中有提到一点可以把事情同時進行,也就是说一次可以同時做好幾件的事情,不需要等前面的事情完成後,才能做後面的任務。
经过启发后的小明就很开心地将一次可以同時做好幾件的事情的方式套用在他出门前的准备事项。
于是他可以去刷牙前,先按下咖啡的按钮、又按下电锅的按钮,他就可以跑去刷牙了。所以刷牙、泡咖啡、準備午餐三件事情在同一時間內一起進行。因此原本要花30分钟时间,一瞬间就可以缩短为10分钟就完成了😍
非同步:一次同时做好几件的事情
为什么Javascript 需要非同步呢🤔
我们刚刚说过同步就是一次完成一件事情,不管後面有幾件事情,都需等待上一件事情的完成才能依序執行。
至于为什么Javascript 需要非同步呢?也许带入情境会更有感觉:
小明在公司午休的时候边吃他早上用电锅蒸好的中餐,边浏览网站。
在Javascript『同步』的情境下,在电脑上的操作都需要等待前一件事情的完成,所以会是怎么样的情况呢?让我们用简单来模拟一下吧😀
上面的例子当然是有点夸张了😅
若使用同步的方式向伺服器端发出请求的话,使用者就會被強迫等待伺服器處理完,並且回傳資料回來的這段時間,而這段時間網頁可能會呈現使用者任何的操作都沒有反應的狀態,因此會讓使用者誤會以爲是當機了,直到资料送回来的任务已经完成后,网页才恢复有反应的状态。
问题点:Javascript 是一个在浏览器执行的语言(先不聊Node.js 啦😆),让使用者必须强迫等待上一个任务执行完毕之后,网页才会有反应的话,会造成极差的使用者体验!😭
因此预期想要达到的效果是:
当一个任务需要花一段时间的时候(例如向伺服器发出请求取得资料),让使用者不需要等待也可以继续地使用网页,等待资料回来之后,在显示在使用者眼前,不会因此而中断使用体验。
还记得非同步吗?因为非同步可以一次同時做好幾件的事情,让浏览器可以在发出请求的同时,一样可以继续回应使用者的操作,并不会因此而让使用者无法操作网页😍
非同步请求
上述的情境其实就是非同步請求(Ajax Asynchronous JavaScript and XML)的使用情境。若对Ajax的用法有兴趣的大大,请先参考W3school的教学😆
Javascript 为单一执行绪
意思就是Javascript 一次只能做一件事情,等上一件事情完成才能執行下一件事情,就如同上述的同步的样子。
那Javascript为什么可以做到非同步的效果呢?🤔
Web APIs
当我们撰写网页程式码时候,有很多常用方便的功能浏览器已经替我们提供好了,他们统称叫做Web APIs。
例如:Dom物件的操作、Ajax相关的XMLHTTPRequest、Fetch API,以及时常被使用到的setTimeout等等。
在呼叫Web APIs时,很常会使用非同步的方式处理。
我们简单用setTimeout看看非同步的效果吧。
setTimeout
setTimeout() 可以为一段程式码设定一个记时计,预计在一段时间后在执行这段程式码。
setTimeout ( function /*要执行的callback function */, delay /*延迟时间(毫秒) */) ;
以下的程式码就会在3000毫秒(3秒)过后,才跳出hihihi~的alert讯息。
setTimeout ( function () {
alert( 'hihihi ~' );
}, 3000 );
console .log( 'a' );
//我们将console.log('b')放入setTimeout的程式码里面,等待0秒
setTimeout ( function () { console .log( 'b in setTimeout' ); }, 0 );
console .log( 'c' );
结果
a
c
b in setTimeout
上面程式码执行的顺序因为非同步的关系变成以下:
执行第1 行,在console 呈现 a
执行第8 行,在console 呈现 c
执行第5 行,在console 呈现b 👈 最后执行
现在知道了使用Web APIs可以达到非同步的效果,但是为什么会是上述的这个结果呢?🥸
Event Loop
在回答上述的问题前,请容许我外插一个Event Loop 的话题,我们先来看个大概的流程图吧😆
我们可以从上面观察到三大区块,分别是:Call Stack、Web APIs、Callback Queue
由上面的顺序,我们会看到Call Stack会呼叫Web APIs,Web APIs等到完成后,会将完成后要执行的callback function丢到Callback Queue内排队,等待Call Stack若是空的状态的话,就可以将要执行的callback function在Call Stack执行。
了解大概的顺序后,我们就来一个个了解这些Keyword 吧😆
Call Stack
在我们撰写Javascript的function并且呼叫的时候,都有默默地使用到Call Stack,只是我们不知道原来我们用过的机制。
Call Stack是一种Javascript用来追踪Function执行状态的机制,他有着『後進先出(Last-In-First-Out, LIFO)』的特性。
假如一段简单的小程式:
function A () { console .log( 'A Start' ); B(); console .log( 'A End' ); }
function B () { console .log( 'B Start' ); console .log( 'B End' ); }
A(); //呼叫function A
我们就会在Console 看到一下的输出
A Start
B Start
B End
A End
Call Stack 过程分解图
接着我们来用慢动作示意图分解看一下在这段程式码的过程发生了什么事情吧😆
到12 行,执行A(),并且将A() 放入Call Stack 内
到2 行,执行console.log('A Start'),并且将它放入Call Stack 内
执行完console.log('A Start'),并且将内容显示在Console 上,将console.log('A Start') 从Call Stack 中移除
到3 行,执行B(),并且将B() 放入Call Stack 内
到8 行,执行console.log('B Start'),并且将它放入Call Stack 内
执行完console.log('B Start'),并且将内容显示在Console 上,将console.log('B Start') 从Call Stack 中移除
到9 行,执行console.log('B End'),并且将它放入Call Stack 内
执行完console.log('B End'),并且将内容显示在Console 上,将console.log('B End') 从Call Stack 中移除
执行完B(),并且从Call Stack 中移除
到4 行,执行console.log('A End'),并且将它放入Call Stack 内
执行完console.log('A End'),并且将内容显示在Console 上,将console.log('A End') 从Call Stack 中移除
执行完A(),并且从Call Stack 中移除
以上的程式码全部执行完毕啦!
从上面的图解过程就可以简单发现一件事情,Call Stack依循着『後進先出(Last-In-First-Out, LIFO)』的特性在记录并且执行函数的状态,A()在第一张图最先跳进Call Stack内,却在倒数第二张图才被移出来。
以上是用图解的方式呈现,若想看动态的执行的话,可以在loupe看到即时的执行呈现喔😍
后进先出(Last-In-First-Out LIFO)
Last-In-First-Out, LIFO
想像手上有一叠扑克牌,在手上的由上到下顺序是1, 2, 3, 4,第一张抽出的为1,并且将1放在桌面,接着继续抽出2放在1的上面,接着将3叠在2的上面,接着放4在3的上面。因为4是最后才被放上去的,所以在最上面,因此当要从桌面取牌的时候,第一张会拿到的为4,接着为3、2、1
Web APIs & Callback Queue
刚刚有提到setTimeout 为Web APIs 的功能,因此当执行这段程式码的时候会走以下的流程
console .log( 'a' );
//我们将console.log('b')放入setTimeout的程式码里面,等待0秒
setTimeout ( function () { console .log( 'b in setTimeout' ); }, 0 );
console .log( 'c' );
当执行到第4 行的时候,会将这段程式码移动到Web APIs 的区块内,等待0 秒
将setTimeout 的Callback Function 放入Callback Queue 内排队
若Call Stack 为空的状态
就将Callback Queue 的Callback Function 推到Call Stack 执行
相对于Call Stack的『後進先出(Last-In-First-Out, LIFO)』, Callback Queue却是先進先出(First-In-First-Out, FIFO)的运作模式。
先进先出First-In-First-Out FIFO
最简单的解释就是用排隊吃飯概念的概念来理解:越早排队的人,应当最早进入餐厅,最后排队的人,则是最后进入。不照着这个规则大家会生气气喔!
执行顺序解答
因此我们终于可以来回答在Web APIs 的问题,为什么会呈现以下的顺序呢?
console .log( 'a' );
//我们将console.log('b')放入setTimeout的程式码里面,等待0秒
setTimeout ( function () { console .log( 'b in setTimeout' ); }, 0 );
console .log( 'c' );
a
c
b in setTimeout
在第1行console.log('a')被推入Call Stack之后,马上被执行,因此Console印出a。
在第4行执行setTimeoutAPI方法后,将这段程式码丢进Web APIs的区块内
在第8行console.log('c')被推入Call Stack之后,马上被执行,因此Console印出c。
等setTimeout在Web APIs等待时间失效后(等待0秒), Callback Function被丢入Callback Queue排队,等到Call Stack有空档的时候,在将在Callback Queue排队的第一个Function(也就是
console.log('b in setTimeout');)丢进Call Stack执行。
最后才在Console 印出 b in setTimeout
上述的步骤若想看动态的执行的话,可以在loupe看到即时的执行呈现喔😍
结论
这篇其实本来只是想针对介绍Promise之前的前情提要介绍文,结果写着写着发现好像又牵扯出Event Loop这个话题才能回答上述的问题,本来还想聊聊Javascript的Async Callback的话题,但看看文章的长度还是另开一篇好了😆
我们从一开始先了解Javascript 的同步所带来的不方便,来了解非同步的好处,在一路沿伸探讨到Javascript 可以非同步原因,用Event Loop 模型作为解答。
关于本文
作者: @chan-chan-dev
原文: https://chan-chan-dev.com/js/Async/async-sync-intro/2534378084/
为你推荐
欢迎自荐投稿,前端早读课等你来。