iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 27
0
Modern Web

JavaScript Note系列 第 27

sync 同步 & async 非同步

同步/非同步這個議題,困擾非常多的初學者,不光是字面上的定義,而且同步與非同步執行的方式,更是讓人混淆。

先來說明字面的意思,同步會讓人以為每個任務是一起進行的,其實是一次只做一件事,不會有兩個任務同時進行。

非同步呢?就是每個任務各做各的,不用等其他任務完成,再進行下一個任務。

打個比方,如果以同步來執行資料存取,首先任務是跟資料庫要資料,但中間影響的因素太多,網路速度,伺服器的處理速度等等,這任務沒完成,之後的任務無法執行,整個Web頁面停滯無法做任何事情,就為了等資料回來,很顯然地,這是個非常差的使用體驗。

非同步的執行方式,就是要資料需要時間,沒關係,這段時間Web依舊可以做其他事情,等資料回來了,我們再處理,這的確合理多了。

結合工作堆疊章節一開始所講的,JavaScipt是個單執行緒非同步的程式語言,等等!!!,單執行緒,一次只能做一件任務,非同步,每件任務各做各的,無須等其他任務完成,怎麼有點矛盾阿。

有一點要強調的是,JavaScipt所謂的非同步並不是真的同時執行多個任務,因為單執行緒的特性,JavaScipt一次只能做一件事,那為何要強調非同步這件事,先不要管非同步這三個字,我們來看看JavaScipt是如何執行的。

(() => {
    console.log('任務A');
})();

(() => {
    for (let i = 0; i < Math.pow(10, 9); i++) {}
    console.log('任務B');
})();

(() => {
    console.log('任務C');
})();

結果會依序輸出任務A、任務B、任務C,我們在任務B設了for迴圈,可以發現,任務C因為任務B而卡住了,一段時間之後才出現。
我們了解,函式會被拉到call stack執行,並且採用同步的方式執行,一個接一個。

再看看下面的範例:

(() => {
    console.log('任務A');
})();
setTimeout(() => {
    console.log('任務B');
}, 1000);
(() => {
    console.log('任務C');
})();

https://ithelp.ithome.com.tw/upload/images/20181111/20112573IMqlX4iO4P.png
這邊我們使用setTimeout( )來模擬非同步的行為,可以發現,任務A、任務C馬上出現,過一秒後才出現任務B,這的確符合正常的邏輯,執行任務B需要時間,沒關係,它繼續做,我們接下去執行任務C,等任務B完成,再回報結果。

第一天有提到,事件會進入queue,等待瀏覽器進行處理,接下來要說明queue的部分。

瀏覽器是個極為複雜的應用程式,為了讓使用者順利瀏覽Web,它背後所處理的工作非常之多,不只是JavaScript而已,JavaScript能夠以非同步的方式來達到同時處理多個任務的效果,主要還是得靠瀏覽器提供的功能,setTimeout( )是瀏覽器所提供的功能,不是JavaScript引擎的功能。

當我們執行到setTimeout( ),它一樣會進入call stack,這時它會將callback function另置到他處(不在call stack)開始計時,setTimeout( )依舊在call stack執行任務,各做各的。

setTimeout( )結束從call stack移出。當callback function計時結束後,會被放入queue等待處理,等到call stack所有任務處理完了,它才會被拉進call stack進行處理。

callback function在它處繼續計時,而我們的call stack繼續執行,這就是非同步所採取的方式。

Event Loop

回顧剛剛的流程,call stack才是真正執行JavaScript的地方,queue裡面放的是等待處理的事件,譬如剛剛的callback function,當call stack所有任務執行完了,是空的,瀏覽器才會去queue依序把事件拉到call stack處理,直到queue變空的,瀏覽器會一直監控call stack和queue,有事件就拉到call stack處理,這就是Event Loop

那如果改成這樣呢?把setTimeout( )移到最上方,計時設為0:

setTimeout(() => {
    console.log('任務A')
}, 0);

(() => {
    console.log('任務B')
})();

(() => {
    console.log('任務C')
})();

https://ithelp.ithome.com.tw/upload/images/20181111/20112573qoIs4Wv0UD.png
結果還是一樣,任務B、任務C先出,最後才是任務A,不管怎樣setTimeout的callback function一定會進入queue變成事件,等call stack空了,再處理。

還有一點,即使把timer設為0,但根據W3C的規範,凡是小於4毫秒的,一律會加到4毫秒,所以,不會有0毫秒的情況發生。
https://www.w3.org/TR/2011/WD-html5-20110525/timers.html

使用setTimeout( )有個重點注意,callback function不一定會在timer指定的時間執行。

確切來說,timer是指,從開始計時到丟置queue的這段時間,萬一queue前面有一堆事件要處理,還是得乖乖排隊,不要忘記了JavaScipt是個單執行緒,即使是queue也是一件一件處理。

我們再加入一個事件:

(() => {
    console.log('任務A');
})();
document.addEventListener('click', () => {
    alert('onClick');
});
setTimeout(() => {
    console.log('Hello World');
}, 1000);
(() => {
    console.log('任務B');
})();

因為任務A跟任務B是直接在call stack就可以完成的函式,所以一定會先完成,然後我們加了事件監聽,只要有滑鼠點擊的事件發生,它就會把callback function送到queue等待處理,setTimeout( )則是跟前的範例一樣,最後出現。
這個例子有兩個狀態監控一個是click事件,另一個則是Event Loop。

最後要說的是,整篇範例都是用setTimeout( )來處理非同步,不表示非同步只有setTimeout( )可以使用,還有非常多的非同步處理,setTimeout只是要模擬使用AJAX request的狀態,需要花點時間,但不會使整個網頁阻塞(blocking)。


上一篇
callback function
下一篇
Promise
系列文
JavaScript Note31

尚未有邦友留言

立即登入留言