同步/非同步這個議題,困擾非常多的初學者,不光是字面上的定義,而且同步與非同步執行的方式,更是讓人混淆。
先來說明字面的意思,同步會讓人以為每個任務是一起進行的,其實是一次只做一件事,不會有兩個任務同時進行。
非同步呢?就是每個任務各做各的,不用等其他任務完成,再進行下一個任務。
打個比方,如果以同步來執行資料存取,首先任務是跟資料庫要資料,但中間影響的因素太多,網路速度,伺服器的處理速度等等,這任務沒完成,之後的任務無法執行,整個Web頁面停滯無法做任何事情,就為了等資料回來,很顯然地,這是個非常差的使用體驗。
非同步的執行方式,就是要資料需要時間,沒關係,這段時間Web依舊可以做其他事情,等資料回來了,我們再處理,這的確合理多了。
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');
})();
這邊我們使用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')
})();
結果還是一樣,任務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)。
JavaScipt是個單執行緒非同步的程式語言
這句話不對。。。依照這邊資料
JavaScript is always synchronous and single-threaded. If you're executing a JavaScript block of code on a page then no other JavaScript on that page will currently be executed.
JavaScript is only asynchronous in the sense that it can make, for example, Ajax calls. The Ajax call will stop executing and other code will be able to execute until the call returns (successfully or otherwise), at which point the callback will run synchronously. No other code will be running at this point. It won't interrupt any other code that's currently running.
https://stackoverflow.com/questions/2035645/when-is-javascript-synchronous
了解,感謝指正