同步與非同步
同步的意思是一次只處理一次事件,而非同步的意思是同時多個事件一起處理。如下
同步執行
同步執行就是將程式碼一行一行執行下來
console.log("Beginning")
console.log("Hello World")
console.log("Ending")
執行結果:
Beginning
Hello World
Ending
非同步執行
非同步執行就是同時執行多個程式碼,而 setTimeout 就是個非同步執行的程式碼,我們稱之為非同步事件,你可以發現輸出結果,竟然是先打印出 End
,再打印出 Hello World
。直觀的想不是應該要等待 0 秒後就直接打印出來嗎?這個問題於之後會解答
console.log("Beginning")
setTimeout(() => {
console.log("Hello World")
}, 0)
console.log("Ending")
執行結果:
Beginning
Ending
Hello World
了解甚麼是同步與非同步之後,其實有一個 Bug 就是 JavaScript 是單執行續的程式,她沒有其他的執行緒能使用啊,到底是如何做到非同步的效果。這就與 JavaScript 的環境和 JavaScipt 的事件佇列有關,而這樣對事件處理的過程就稱為 Event Loop。接下來將會對 JavaScript 在瀏覽器環境下的 Event Loop 和 Node.js 環境下的 Event Loop 去做講解。
JavaScipt 在 Browser 上使用
在 Browser 環境下,遇到非同步事件的處理過程如下:
名詞介紹
Call Stack
Call Stack 的角色就是執行緒,將要被執行的程式放入 Call Stack,接著在由上往下執行
- 同步事件立刻執行
- 非同步事件放到 Callback Queue 等待下一次執行
Web Apis
Web Apis 的角色就是補充包,若在 Call Stack 中出現 Web Apis 可支援的方法,如:setTimeout,則從 Web Apis 取出放入 Callback Queue 等待下一次執行
Callback Queue
Callback Queue 的角色就是事件佇列,所有非同步事件會先被放入這裡,等待 Call Stack 清空後,再將裡面的所有程式放入 Call Stack
流程說明
Demo 1
說明:非同步呼叫 web apis 的流程
const A = () => console.log("Beginning")
const B = () => console.log("Ending")
const C = () => console.log("Function C")
const D = () => console.log("Function D")
console.log("Beginning")
setTimeout(() => D(), 300)
setTimeout(() => C(), 100)
console.log("Ending")
Beginning
Ending
Function C
Function D
圖解說明:
- Function A 是同步事件,所以直接執行
- setTimeout 是非同步事件,呼叫 web Apis
- Function B 是同步事件,所以直接執行
- setTimout 先完成的先放入 Callback queue,所以是 Function C 和 Function D
- Call Stack 清空後,將 Function C、Function D 放入
- Function C 和 Function D 都是同步事件,所以依序執行
- Callback Queue 清空,程式結束
Demo 2
說明:同時使用 Promsie 與 web apis 的執行順序
const A = () => console.log("Beginning")
const B = () => console.log("Ending")
const C = () => console.log("Function C")
const D = () => console.log("Function D")
const E = () => console.log("Function E")
console.log("Beginning")
setTimeout(() => C(), 0)
new Promise(function(resolve, reject) {
D()
resolve('resolve')
}).then(result => {
E()
})
console.log("Ending")
Beginning
Function D
Ending
Function E
Function C
- Function A 是同步事件,所以直接執行
- setTimeout 是非同步事件,呼叫 web Apis
- Promise 是同步事件,所以直接執行
- 但是 Promise 的 resolve 和 recject 結果則是非同步事件,所以放入 Callback queue
- setTimout 放入 Callback queue
- Call Stack 清空後,將 Function E、Function C 放入
- Function E 和 Function C 都是同步事件,所以依序執行
- Callback Queue 清空,程式結束
JavaScript 在 Node.js 上使用
JavaScript 在 Node.js 環境下,和 Browser 環境不同的是,沒有 web apis 可以呼叫,但可以呼叫 node.js 的內建方法,此外還加入了 I/O 操作和 process object 的概念,所以使得非同步事件的處理更為複雜。不過基本上 Event Loop 沒有太大變化,只是多了非同步事件的優先順序要判斷而已,接下來要介紹非同步事件種類的處理順序
名詞介紹
nextTick Queue
此 queue 的優先級別最高
process.nextTick()
的 callback function
都會來這裡
microTask Queue
此 queue 的優先級別第二高
resolve
或 reject
所執行的 callback function
會被排在這裡
macroTask Queue
當上面兩種 queue 都清空時,才會開始跑這些事件
- timers:處理計時器相關的事件,例如
setTimeout
和setInterval
設定的事件 - pending callbacks:處理一些系統級別的回調
- idle、prepare:node.js 內部使用
- poll:查詢 I/O 操作的事件,例如讀取文件,網路請求等
- check:處理
setImmediate()
設定的事件 - close callbacks:處理一些關閉事件的回調
流程說明
Demo 1
說明:多種非同步事件執行順序
console.log("Beginning")
process.nextTick(function() {
console.log('nextTick1');
});
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve('resolve');
}).then(function(result) {
console.log('promise then');
})
(async function() {
console.log('async');
})();
setImmediate(function() {
console.log('setImmediate');
});
process.nextTick(function() {
console.log('nextTick2');
});
console.log('Ending');
Beginning
promise
async
Ending
nextTick1
nextTick2
promise then
setTimeout
setImmediate
- Beginning 是同步事件,直接執行
- nextTick1 代表下一次的執行序,所以放到 nextTick Queue
- setTimeout 是非同步事件,放到 macroTask Queue
- Promise 是同步事件,所以直接執行
- 但是 Promise 的 resolve 結果要放到 microTask Queue
- async 是同步事件,直接執行
- setImmediate 是非同步事件,放到 macroTask Queue
- nextTick2 代表下一次的執行序,所以放到 nextTick Queue
- Ending 是同步事件,直接執行
- 從 nextTick Queue 開始取出放入 call Stack,接著反覆執行直到 nextTick Queue 是空的
- 從 microTask Queue 開始取出放入 call Stack,接著反覆執行直到 microTask Queue 是空的
- 若 nextTick Queue 和 micro Task Queue 都清空後,才從 macroTask Queue 開始取出放入 call Stack
- 反覆執行直到所有 Queue 和 Stack 清空為止
Demo 2
說明:當非同步事件中又有其他非同步事件時的執行順序
console.log("Beginning")
process.nextTick(function() {
console.log("nextTick1")
process.nextTick(() => {
console.log("nextTick2")
})
});
setTimeout(function() {
new Promise(function(resolve, reject) {
console.log('promise');
resolve('resolve');
}).then(result => {
process.nextTick(() => {
console.log("nextTick3")
})
}).then(result => {
console.log('promise then');
})
console.log('setTimeout');
}, 0);
console.log('Ending');
Beginning
Ending
nextTick1
nextTick2
promise
setTimeout
promise then
nextTick3
- Beginning 是同步事件,直接執行
- nextTick1 代表下一次的執行序,所以放到 nextTick Queue
- setTimeout 是非同步事件,放到 macroTask Queue
- Ending 是同步事件,直接執行
- 從 nextTick Queue 取出,nextTick1 執行,遇到 nextTick2 放入 nextTick Queue
- 從 nextTick Queue 取出,nextTick2 執行
- 由於 nextTick Queue 和 microTask Queue 都是空的,所以從 macroTask Queue 取出
- setTimeout 執行,遇到 Promise 先執行,因為 Promise 創建時是同步事件,接著將 resolve 放入 microTask Queue
- setTimout 繼續執行打印出 setTimout
- 從 microTask Queue 取出,先遇到 nextTick3 放入 nextTick Queue,繼續執行打印出 promise then
- 從 nextTick Queue 取出 nextTick3 執行
- 所有的 stack 和 queue 都清空,程式結束
總結
JavaScript 是藉由 call Stack (單執行緒) 和 callback Queue (事件佇列) 實現多執行緒的效果。在 Browser 中,非同步事件主要與 web 有關,所以會需要經常從 web Apis 呼叫所需要的方法。而 Node.js 中,非同步事件的種類更為複雜,所以設計了 nextTick Queue、microTask Queue 和 macroTask Queue 等 callback Queue 的設計來分類處裡,所以只要弄清楚現在的非同步事件是甚麼類型,就可以搞懂那些奇怪的輸出結果。
但仔細想想非同步事件只是重新安排程式的處理順序而已,並沒有比較快。沒錯,只是被重新安排的程式不會是非同步事件本身,而是非同步事件內的其他程式,如下:
setTimeout(() => console.log("Hello"), 2000)
當我們遇到了 setTimeout
這個非同步事件,我們會讓他開始計時兩秒,接著跳過繼續執行下一個程式。當計時結束時,才會把 console 放入事件佇列,所以被重新安排的其實是 console 這個程式,而不是把 setTimeout 整個放到最後執行
簡單來說就是讓這個非同步程式先開始執行,如連結資料庫,抓取 api,計時等,當程式執行完的結果就放到事件佇列等待。因此在處理較耗時的程式時,就不容易造成程式堵塞,建議在較大型、複雜的應用上,能採用非同步事件的設計來加速程式運作的過程。
資料來源
comments powered by Disqus