[node.js]非同步(Asynchronous)、同步(Synchronous)、callback

簡單介紹 node.js 裡重要的「非同步(Asynchronous)」概念,那既然有「非同步(Asynchronous)」當然就會有「同步(Synchronous)」,以及因為「非同步(Asynchronous)」而衍伸出的重要 callback 概念。


Node.js 是一個單執行緒且非同步的語言。

note

  • 執行緒:執行緒是比程序更小的單元,它是 cpu 的最小執行單元。是作業系統能夠進行運算排程的最小單位。一個程序,至少包含一個或多個執行緒。(source)

  • 單執行緒:單執行的特性是順序執行,當遇到比較耗時的任務時,還未執行的任務就會處於等待狀態,一定要等到前面的任務完成了,才會往後執行。(source)

Asynchronous(非同步)& Event queue(事件佇列)

以上提到,單執行緒「還未執行的任務就會處於等待狀態」,所謂的「未執行任務」必須要是非同步(Asynchronous)function 才行,而「等待狀態」是指那些任務會被放到 Event queue,而 Event queue 的事件在所有事件完成前不會被執行。簡單的說,程式碼一行一行執行,當遇到非同步(Asynchronous)function 時,就會先執行下一行的任務,而這個還未被執行的非同步(Asynchronous)function 就會被放到 Event queue 中等到最後才執行。


同步(Synchronous)

  • 指程式必須等待前面的程式執行完才能執行。

如果 function 是同步(Synchronous),那就不管如何都會等這行執行完才執行下一行程式碼。例如,writeFileSync()就是同步(Synchronous) function。但先等這行程式碼執行完再執行下一行有什麼問題呢?假如今天要寫的檔案內容超級大,那麼所有工作就會全部停擺,等到這行程式碼完成才能進行,而非同步(Asynchronous)function 便能解決這個問題。

非同步(Asynchronous)

  • 指程式不須等待前面的程式執行完就能執行。

下面舉一個常見的非同步 function,setTimeout()(第一個參數為時間到時要被執行的函式,第二個參數為要延遲的時間(毫秒)),setTimeout() 會在第二個參數的延遲時間後執行第一個參數的函式。

setTimeout(() => {
	console.log('Timer is done!')
}, 1);

console.log('Hello');
console.log('Hi');

以上程式碼印出來的結果會是,

Hello
Hi
Timer is done!

因為 setTimeout() 裡的任務被放到 Event queue 中了,必須等到下面兩行程式碼跑完才接著跑 setTimeout() 裡的東西。

callback

但假如果我們現在就是想要讓程式執行完,才接著下一個任務該怎麼辦呢?所以這時候有了 callback

callback 讓我們把一個函式丟進另一個函式當參數,讓我們可以控制程式碼的流程。先看一個簡單的範例,

function callbackSleepWorker() {
  alert('OK, Im wake up !')
}
function ICallYouWhenIDone(callbackWorker) {
  alert('OK, Im first.')
  callbackWorker()
}
ICallYouWhenIDone(callbackSleepWorker)

這個結果會先印出 ‘OK, Im first.’ 接著再印出 ‘OK, Im wake up !’。

上面這個例子大概還看不太出來為什麼需要 callback,如果現在這個函式是個非同步函式呢?

let money = null
function slower() {
  setTimeout(function() {
    money = 30
  }, 200)
}
function faster() {
  setTimeout(function() {
    console.log('I have ' + money)
  }, 100)
}
slower()
faster()

以上程式碼印出來的會是:I have null。

明明是先跑了 slower() 但 money 卻沒先被負值,這是因為 setTimeout() 是個非同步 function,所以它會先被放到 event queue 中,faster() 就先執行了。那如果我們現在想要讓 slower() 百分之百先執行可以怎麼做呢?

let money = null
function slower(callbackWorker) {
  setTimeout(function() {
    money = 30
    callbackWorker()
  }, 200)
}
function faster() {
  setTimeout(function() {
    console.log('I have ' + money)
  }, 100)
}
slower(faster)

這時候印出來的結果就會是:I have 30

現在有個任務是,讀檔案並計算長度

var fs = require('fs');

// fs.readFile(filename, callback(err, content))

fs.readFile('test.txt', function(err, content){
    var str = content.toString();
    console.log(str.length);
    console.log('finish');
});

console.log('not finish');

本來 readFile() 這個 function 會先被放到 event queue 裡,然後接著印出 ‘not finish’,但我們現在把要讀檔案這個任務放在 callback 裡,所以變成只要讀完檔案就會立刻執行,’not finish’ 會等到檔案讀完才會進行。


Reference:
JS20min Day — 18 關於回呼生活化 (Callback)
[Node.js] 理解 Node.js 事件驅動
JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
鐵人賽:一次只能做一件事情的 JavaScript
JavaScript 核心篇 學習筆記: Chap.15–執行緒與同步/非同步