# Explain the difference between synchronous and asynchronous functions. ::: info :bookmark:同步函數會阻塞 (blocking) 。 同步函式中,每個 statements 會在下一個 statements 執行前完成。這種情況下,程式會按照順序進行精確評估,如果其中一個 statements 執行花了很長的時間,程式就會停住,直到執行完成才會繼續跑。 :bookmark:非同步函數不會阻塞 (non-blocking) 。 非同步函式通常接收 callback 作為參數,並且在非同步函式呼叫後立即執行下一行。 callback 僅會在非同步程式操作完成和 call stack 清空時被呼叫。 :bookmark: 從 web server 載入資料或是查詢 database 之類的繁重操作,都應該以非同步完成,以讓 main thread 可以持續執行其他的操作。(因為以瀏覽器的角度來看,同步的操作會使得 UI 凍結(freeze)) ::: [阻塞 blocking( Promise, Async / Await篇有提到 )](https://hackmd.io/30UT3xH5TyOFHUJRuLQ9Uw#%E3%80%90%E3%80%91Blocking-%E9%98%BB%E5%A1%9E) **main thread 主執行緒**:在瀏覽器裡面,負責執行 JavaScript 的叫做 main thread,負責處理跟畫面渲染相關的也是 main thread。 --- ## JavaScript 本身是同步的 JavaScript 基本上是一個同步,且跑在單一執行緒 (Single-Thread) 的程式語言,也就是在同一時間只能執行一個操作。 ### 那為什麼可以執行非同步事件呢? 因為當在執行 Javscript 的時候,Javascript 是在瀏覽器內執行的。 瀏覽器這個執行環境 (runtime) 還提供了很多 WebAPI (ex: document、XMLHttpRequest、setTimeout) 給我們使用,他們不在 V8 引擎中,也是我們無法取得的內容,所以我們只能呼叫這些功能去執行他。 當瀏覽器知道你要呼叫他們來用的時候,就可以和你的程式碼同時一起執行 ,也不會影響到你的 JS 主程式。 **==所以在瀏覽器內,只有 JavaScript 引擎本身是同步的,而 Javascript 引擎可以跟 WebAPI 溝通,達到非同步的事件處理。==** ## 生活中的同步與非同步 ### 想像你去咖啡店買拿鐵和黑咖啡: **==同步 Synchronous==** 1. 店員在收銀機上輸入你的點餐內容 2. 店員請 A 同事開始準備拿鐵 3. A 同事準備好拿鐵,店員轉交給你 4. 店員請 B 同事開始準備黑咖啡 5. B 同事準備好黑咖啡,店員交給你 6. 店員幫你結帳 **==非同步 Asynchronous==** 1. 店員在收銀機上輸入你的點餐內容 2. 店員請 A 同事準備拿鐵、B 同事準備黑咖啡。B同事先完成了,剛好店員幫你結完帳沒事,所以先把黑咖啡拿給你。 3. 拿鐵製作包含較多步驟,花費比較多時間,等 A 同事完成後,店員剛好沒事,所以把拿鐵也遞給你。 從咖啡店的例子複習專有名詞: 1. 同步的例子裡,店員一次就只能做一件事情 ==(Single thread 單執行緒)==; <br/>而在非同步的例子中,將製作咖啡的事情委派給其他同事處理,讓自己可以繼續幫你結帳,來提高效率。 **JavaScript 是 Single thread,一次只能做一件事情。** 2. 非同步的例子中,店員將製作咖啡的任務發出去後,其實不知道哪一個任務會先完成,但卻可以繼續自己結帳的流程,不會因為同事 A、B 還在製作咖啡,就不能接下去動作 ==(不會阻塞 non-blocking)== 。<br>JavaScript 一次只能做一件事,但藉由瀏覽器或 Node 提供的 API 協助,在背後處理這些事件(想像成同事在背後製作咖啡),得以在等待製作的同時,不會 ==阻塞(blocking)== 到下一件事的執行(想像成店員可以繼續結帳流程)。 <br/> ## 瀏覽器的同步與非同步 ### 假設我們今天在 JavaScript 有一個函式叫做 getAPIResponse,能串接後端的 API 拿取資料。 ### 同步的寫法: ``` javascript= // JavaScript 執行這行會一直等到回傳資料後,才會繼續執行下一行程式碼。 const response = getAPIResponse() console.log(response) ``` 假如今天要等到 10 秒後,API Server 才會回傳資料,這段期間等於說讓執行 JavaScript 的執行緒(thread)凍結。 在瀏覽器裡面,負責執行 JavaScript 的叫做 main thread,負責處理跟畫面渲染相關的也是 main thread。 如果這個 thread 凍結 10 秒,會造成無法點擊畫面,因為瀏覽器沒有資源去處理其他事情。 而瀏覽器裡執行 JavaScript 的 main thread 同時也負責畫面的 render,因此非同步變得很重要。否則等待的時候畫面會凍結,就像電腦當機。 ### 將程式改成非同步,使用 callback function 來接收結果: ``` javascript= // 寫法一:額外宣告函式 function handleResponst() { console.log(response) } getAPIResponse(handleResponst) // 寫法二:匿名函式 getAPIResponse(function(err, response) { console.log(response) }) // 寫法三:利用 ES6 箭頭函式簡化過後 getAPIResponse((err, response) => { console.log(response) }) ``` 以 AJAX 來看: ``` javascript= var request = new XMLHttpRequest() request.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true); request.onload = function() { if (this.readyState == 4 && this.status == 200) { console.log(this.response) } } request.send(); ``` 這邊的 callback function 就是 request.onload = 後面的那個函式, 代表「當 response 回來時,請執行這個函式」。 :::success **常見的一些非同步 callback function** 複習 callback function,它的意思是「當某事發生的時候,請利用這個 function 通知我」 ``` javascript= // 例子1 const btn = document.querySelector('.btn_alert') // 當有人點擊 .btn_alert 這個按鈕時, // 請利用這個 function(handleClick)通知我 btn.addEventListener('click', handleClick) // handleClick 就是 callback function function handleClick() { alert('click!') } // 例子2 // 當網頁載入完成發生時,請利用這個 function(匿名函式)通知我 window.onload = function() { alert('load!') } // 例子3 // 當過了兩秒,請利用這個 function(tick)通知我 setTimeout(tick, 2000) function tick() { alert('時間到!') } ``` :zap: **然而要注意的是,不是所有的回呼都是非同步的——有些是跑在同步上。** 比如說當我們使用forEach() 在陣列裡面用迴圈來遍歷每一個項目: ``` javasrcipt= const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus']; gods.forEach(function (eachName, index){ console.log(index + '. ' + eachName); }); ``` ::: 更多的非同步技術請參考之前的:[Promise, Async / Await 篇](https://hackmd.io/30UT3xH5TyOFHUJRuLQ9Uw#%E3%80%90%E3%80%91Blocking-%E9%98%BB%E5%A1%9E) ## 非同步小測驗 ### 1. 執行順序 ``` javascript= a(function() { console.log('a') }) console.log('hello') // 請問:最後的輸出順序為何?是先 hello 再 a,還是先 a 再 hello // 還是不一定? ``` :::spoiler answer 答案是不一定。 因為沒有說 a 是同步還是非同步的。 a 可以這樣實作: ``` javascript= function a(fn) { fn() // 同步執行 fn } a(function() { console.log('a') }) console.log('hello') ``` 輸出就會是 a 然後 hello。 a 也可以這樣實作: ``` javascript= function a(fn) { setTimeout(fn, 0) // 非同步執行 fn } a(function() { console.log('a') }) console.log('hello') ``` 輸出就是 hello 然後才 a。 ::: ### 2. 慢慢等 期待:ajax 的 response 回來之前會不斷印出 waiting,直到接收到 response 才停止。 ``` javascript= let gotResponse = false getAPIResponse('/check', () => { gotResponse = true console.log('Received response!') }) while(!gotResponse) { console.log('Waiting...') } ``` 請問:以上寫法可以滿足期待嗎?如果不行,請詳述原因。 :::spoiler answer 答案是不行。 還記得 event loop 的條件嗎?「當 call stack 為空,才把 callback 丟到 call stack」。 ``` javascript= while(!gotResponse) { console.log('Waiting...') } ``` 這一段程式碼會不斷執行,成為一個無窮迴圈。所以 call stack 永遠都有東西,一直被佔用,callback queue 裡面的東西根本丟不進 call stack。 因此原本的程式碼無論有沒有拿到 response,都只會一直印出 waiting。 ::: ### 3. 詭異的計時器 小明被主管指派要去解一個 bug,在公司的程式碼裡面找到了這一段: ``` javascript= setTimeout(() => { alert('Welcome!') }, 1000) // 後面還有其他程式碼,這邊先略過 ``` 這個 bug 是什麼呢? 就是這個計時器明明指定說 1 秒之後要跳出訊息,可是執行這整段程式碼(注意,底下還有其他程式碼,只是上面先略過而已)以後,alert 卻在 2 秒以後才跳出來。 請問:這有可能發生嗎?無論你覺得可能或不可能,都請試著解釋原因。 :::spoiler answer 答案是有可能。 WebAPI 會在一秒之後把 callback 丟到 callback queue,那為什麼兩秒之後才會執行呢?因為這一秒 call stack 被佔用了。 只要 setTimeout 底下的程式碼做了很多事情並佔用了一秒鐘,callback 就會在一秒之後才被丟到 call stack 去,例如說: ``` javascript= setTimeout(() => { alert('Welcome!') }, 1000) // 底下這段程式碼會在 call stack 佔用一秒鐘 const end = +new Date() + 1000 while(end > new Date()){ } ``` 所以 setTimeout 只能保證「至少」會在 1 秒後執行,但不能保證 1 秒的時候一定執行。 ::: --- [JavaScript 中的同步與非同步(上):先成為 callback 大師吧!](https://blog.huli.tw/2019/10/04/javascript-async-sync-and-callback/)