阻塞與非阻塞 blocking & non-blocking
- 阻塞(blocking)代表執行時程式會卡在那一行,直到有結果為止,例如說readFileSync,要等檔案讀取完畢才能執行下一行
- 非阻塞(non-blocking)代表執行時不會卡住,但執行結果不會放在回傳值,而是需要透過回呼函式(callback function)來接收結果
同步與非同步 synchronous & asynchronous
node.js 部分
Node.js 的官方文件說:
Blocking methods execute synchronously and non-blocking methods execute asynchronously.
阻塞的方法會同步地(synchronously)執行,而非阻塞的方法會非同步地(asynchronously)執行
簡單舉例:
在又要讀檔又要印出偶數的範例中,同步指的就是彼此互相協調互相等待,所以讀檔還沒完成的時候,是不能印偶數的,印出偶數一定要等到讀取檔案結束之後才能進行。
非同步就是說各做各的,你讀檔就讀你的,我繼續印我的偶數,大家腳步不一致沒關係,因為我們本來就不同步。
在討論到 JavaScript 的同步與非同步問題時,基本上你可以把非同步跟非阻塞劃上等號,同步與阻塞劃上等號。如果你今天執行一個同步的方法(例如說readFileSync),就一定會阻塞;如果執行一個非同步的方法(readFile),就一定不會阻塞。
重點:
- 同步(synchronous)代表執行時程式會卡在那一行,直到有結果為止,例如說readFileSync,要等檔案讀取完畢才能執行下一行
- 非同步(asynchronous)代表執行時不會卡住,但執行結果不會放在回傳值,而是需要透過回呼函式(callback function)來接收結果
瀏覽器上的同步與非同步
同步寫法:
const response = getAPIResponse()
console.log(response)
同步會發生什麼事?就會阻塞後面的執行,所以假設 API Server 主機規格很爛跑很慢需要等 10 秒,整個 JavaScript 引擎都必須等 10 秒,才能執行下一個指令。
如果把 JavaScript 的執行凍結在那邊 10 秒,就等於說讓執行 JavaScript 的執行緒(thread)凍結 10 秒。在瀏覽器裡面,負責執行 JavaScript 的叫做 main thread,負責處理跟畫面渲染相關的也是 main thread。換句話說,如果這個 thread 凍結 10 秒,就代表你怎麼點擊畫面都不會有反應,因為瀏覽器沒有資源去處理這些其他的事情。這時候整個畫面就會像當機一樣什麼事都不能做。
所以像是網路這麼耗時的操作,是不可能讓它同步執行的。既然要改成非同步,那依據之前學過的,就要改成用 callback function 來接收結果:
// 底下三個範例,都在做一模一樣的事情
// 範例一
// 最初學者友善的版本,額外宣告函式
function handleResponst() {
console.log(response)
}
getAPIResponse(handleResponst)
// 範例二
// 比較常看到的匿名函式版本,功能跟上面完全一樣
getAPIResponse(function(err, response) {
console.log(response)
})
// 範例三
// 利用 ES6 箭頭函式簡化過後的版本
getAPIResponse((err, response) => {
console.log(response)
})
而實際用 Ajax 呼叫後端 API 的程式碼會長這樣子:
var request = new XMLHttpRequest();
request.open('GET', 'https://jsonplaceholder.typicode.com/users/1', true);
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
console.log(this.response)
}
};
request.send();
這邊的 callback function 就是 request.onload =
後面的那個函式,這一行的意思就是說:「當 response 回來時,請執行這個函式」。
callback
callback function 的意思其實就是:「當某事發生的時候,請利用這個 function 通知我」
例如:
const btn = document.querySelector('.btn_alert')
btn.addEventListener('click', handleClick)
function handleClick() {
alert('click!')
}
「當某事(有人點擊 .btn_alert 這個按鈕)發生時,請利用這個 function(handleClick)通知我」,handleClick
就是 callback function。
初學者易混淆部分
// 錯誤範例
setTimeout(2000, tick())
function tick() {
alert('時間到!')
}
// 上面的錯誤範例等同於
let fn = tick()
setTimeout(2000, fn)
function tick() {
alert('時間到!')
}
上面的意思是tick
是一個 function,tick()
則是執行一個 function,並且把執行完的回傳結果當作 callback function。
由於 tick 執行後會回傳 undefined,所以 setTimeout 那行可以看成:setTimeout(2000, undefined)
,一點作用都沒有。
把 function 誤寫成 function call 以後,會產生的結果就是,畫面還是跳出「時間到!」三個字,可是兩秒還沒過完。因為這樣寫就等於是你先執行了 tick 這個 function。
Callback function 的參數
const btn = document.querySelector('.btn_alert')
btn.addEventListener('click', handleClick)
function handleClick() {
alert('click!')
}
==一個叫做 event 的 object 會被傳進去,而這個 object 是在描述這個發生的事件。==
const btn = document.querySelector('.btn_alert')
btn.addEventListener('click', handleClick)
function handleClick(e) {
console.log(e)
// 這時會印出一個有超多屬性的物件
// 物件其實就是在描述剛剛的「點擊」
}
function 在傳送以及接收參數的時候,注重的只有「順序」,而不是文件上的名稱。文件上的名稱只是參考用的而已,並不代表你就一定要用那個名稱來接收。function 沒有那麼智慧,不會根據變數名稱來判斷是哪個參數。
所以== callback function 參數名稱想要怎麼取都可以==,handleClick(e)
、handleClick(evt)
、handleClick(event)
或是 handleClick(yoooooo)
都可以,都可以拿到瀏覽器所傳的event
這個物件,只是叫做不同名稱而已。
Callback 的 error first 慣例
流程 code 詳見參考資料
在同步的版本當中,我們會等待檔案讀取完畢才執行下一行,所以讀取檔案的時候出了什麼錯,就會把錯誤拋出來,我們就可以 try…catch 去處理。
但是在非同步的版本中,fs.readFile這個 function 只做了一件事,就是跟 Node.js 說:「去讀取檔案,讀取完之後呼叫 callback function」,做完這件事情之後就繼續執行下一行了。
所以讀取檔案那一頭發生了什麼事,我們是完全不知道的。
callback 會有兩個參數,第一個是 err,第二個是 data,這樣你就知道 err 是怎麼來的了。只要在讀檔的時候碰到任何錯誤,例如說檔案不存在、檔案超過記憶體大小或是檔案沒有權限開啟等等,都會透過這個 err 參數傳進來,這個錯誤你用 try…catch 是抓不到的。
所以,當我們非同步地執行某件事情的時候,有兩點我們一定會想知道:
- 有沒有發生錯誤,有的話錯誤是什麼
- 這件事情的回傳值
舉例來說,讀取檔案我們會想知道有沒有錯誤,也想知道檔案內容。或是操作資料庫,我們會想知道指令有沒有下錯,也想知道回傳的資料是什麼。
既然非同步一定會想知道這兩件事,那就代表至少會有兩個參數,一個是錯誤,另一個是回傳值。小標題所說的「error first」,就代表錯誤「依照慣例」通常會放在第一個參數,其他回傳值放第二個以及第二個之後。
先判斷有沒有錯誤再做其他事情:
const fs = require('fs')
fs.readFile('./README.md', (err, data) => {
// 如果錯誤發生,處理錯誤然後返回,就不會繼續執行下去
if (err) {
console.log(err)
return
}
console.log(data)
});
Q: 那為什麼 setTimeout 或是 event listener 這些東西都沒有 err 這個參數?」
那是因為這幾個東西的應用場合不太一樣。
setTimeout 的意思是:「過了 n 秒後,請呼叫這個 function」,而 event listener 的意思是:「當有人點擊按鈕,請呼叫這個 function」。
「過了 n 秒」以及「點擊按鈕」這兩件事情是不會發生錯誤的。
但像是 readFile 去讀取檔案,就有可能在讀取檔案時發生錯誤;而 XMLHttpRequest 則是有另外的 onerror 可以用來捕捉非同步所產生的錯誤。
小總結
- 瀏覽器裡執行 JavaScript 的 main thread 同時也負責畫面的 render,因此非同步顯得更加重要而且必須,否則等待的時候畫面會凍結
- callback function 的意思其實就是:「當某事發生的時候,請利用這個 function 通知我」
- fn 是一個 function,fn() 是執行 function
- callback function 的參數跟一般 function 一樣,是看「順序」而不是看名稱,沒有那麼智慧
- 依照慣例,通常 callback function 的第一個參數都是 err,用來告訴你有沒有發生錯誤(承第一點,你想取叫 e、error 或是 fxxkingError 都可以)
- 非同步還是有可能用 try catch 抓到錯誤,但那是代表你在「呼叫非同步函式」的時候就產生錯誤
Event loop
event loop 的作用:
不斷偵測 call stack 是否為空,如果是空的話就把 callback queue 裡面的東西丟到 call stack
以程式的角度去想,event loop 之所以叫做 loop,就是因為它可以表示成這樣:
while(true) {
if (callStack.length === 0 && callbackQueue.length > 0) {
// 拿出 callbackQueue 的第一個元素,並放到 callStack 去
callStack.push(callbackQueue.dequeue())
}
}
這邊只要掌握一個重點就好:「==非同步的 callback function 會先被放到 callback queue,並且等到 call stack 為空時候才被 event loop 丟進去 call stack==」
Event loop 就是那種只會出一張嘴不會做事的人,它不負責幫你執行 callback function,只會幫你把 function 丟到 call stack,真正在執行的還是 JavaScript 的 main thread。
- 範例:
執行順序一樣是由上到下,只是在 setTimeout 那邊是先把這整段放到 call stack 裡面去執行,所以才會執行 setTimeout 這個 function。然後 setTimeout 會呼叫瀏覽器幫忙設定一個 0 ms 後到期的定時器,到期之後就會把第一個參數:() => {console.log('hello')}
整個 function 放進去 callback queue,
而 call stack 一樣會依序執行同步的指令,call stack 執行完沒東西後 event loop 才會把在 callback queue 排隊的 () => {console.log('hello')}
丟到 call stack 執行。
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
// 執行順序一樣是
// 1
// 3
// 2
錯誤範例
console.log(1) // 放入 Call Stack 並直接執行,印出 1,執行完後移除
setTimeout(() => { // setTimeout() 放到 Webapis 執行,直到倒數完畢,
console.log(2) // () => { console.log(2) } 被放到 Callback Queue 待命
}, 0)
錯誤的點在:「setTimeout() 放到 Webapis 執行」,web api 不是一個地方,是一個種類,setTimeout 是屬於 web api 的其中一個,但是不是 web api 跟非同步無關。
Web API 不是一個地方。這邊可以直接講瀏覽器就好,呼叫 setTimeout 之後叫瀏覽器設定一個計時器,0ms 之後會觸發,那這個計時器設定在哪邊?不重要,這是瀏覽器會去處理的事。
其他錯誤範例:
week16 自我檢討
還有一些同步與非同步小測驗,詳見參考資料
參考資料:
JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
What the heck is the event loop anyway? | Philip Roberts | JSConf EU