前陣子因為要向同事解釋node.js專案的某段程式碼時,他問很多問題,其中一個問題是:
Node.js 是怎麼做到非同步
記得我當時是回答了一個錯得離譜的答案,就不說出來給大家笑了。於是我花了一段時間研究Node.js或者說Javascript是怎麼運作的,以下的心得跟大家分享,如果有任何錯誤,歡迎直接留言指正。
Node.js Javascript code是單一線程(single thread),這意味著,他只有一個call stack, 一次只做一件事。這個限制讓你在開發時不用考慮並發的情況。你只需要思考如果避免任何會卡住這個thread的情況,例如同步的 network calls或無限迴圈。
The call stack
call stack是一個LIFO(Last In, First Out) 佇列,可以讓我們知道我現在在程式中的哪個部分,可以知道哪個function會呼叫哪個function,或是function結束後要return到哪個function…。
常常在debugger或console看到的error stack track就是browser透過查看call stack裡的function name 告訴你哪個現在的呼叫是起源於哪個function。
Blocking
那如果有某段程式碼程式執行起來很慢,例如:ajax,這樣會發生什麼事?前面說過:**javascript是單線程的語言,一次只會做一件事。**所以他會等API回應得到結果後才會繼續做下面的事情,這樣的話call stack就會被卡住,如果我們想有流暢的UI的話就不能卡住call stack。
Asynchronous callbacks
但如果有寫過ajax或是 setTimeout 之類非同步程式的人就知道javascript其實不會像上述說的那樣:等API回來或 setTimeout 的時間到才繼續執行。
當執行到像ajax請求或 setTimeout 時,javascript會繼續執行接下來的程式碼,當ajax得請求回應後或者 setTimeout 的時間到時才會執行callback function裡的程式碼。
那問題就來了:
單線程的javascript是怎麼做到,執行程式碼時同時做其他事呢?
Event loop
上面沒說到的是,像ajax或setTimeout之類的功能其實是browser提供的WebAPIs(Node.js中是libuv),所以其實是有自己的執行緒,不包含在javascript的執行緒中,你只能呼叫他們。
當browser執行完像ajax後,會將callback function放到task queue(或稱為callback queue)裡,這時候如果call stack已經空了, event loop就會抓出來放到call stack裡,然後這個callback function就會被執行。
如果同時做了兩次ajax請求時而且browser都在差不多時間完成且都把各自的callback function放到callback queue時,event loop不會一次把兩個都拿出來。只會依序地一次拿一個,等到call stack空了才會再去callback queue拿下一個。
所以才說不要卡住call stack,因為當call stack卡住了,event loop檢查到call stack裡還有東西,就不會去callback queue抓callback function到call stack。
這樣說有點抽象,我們來實際跑一次:
一開始 console.log(‘Hi’) 會被放到stack裡,執行完後從stack裡pop掉
接著換 $.get(‘url’, …) 被放到stack裡,但因為會做ajax請求,所以後面的callback function會被移到webapis等待回應。同時這行程式碼就結束了,所以從stack裡pop掉。
再來 console.log(‘JSConfEU’) 會被放到stack裡,執行完後從stack裡pop掉
這時ajax請求已經回應了,callback function會被移到task queue裡
event loop看到stack裡空了,就會從task queue抓一個callback function放到stack裡執行。
總結
到了這邊,我簡單的說明了Node.js如何在單線程中做到非同步的工作。以上心得主要參考自:
以及下面這部youtube影片,我覺得講得非常淺顯易懂,加上實際用程式碼解釋整個過程,非常建議看過一次。
One more thing…
在瞭解了Node.js如何運時候,大家來想想看下面這行程式碼會印出什麼?
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
就我們上面說的:Webaips的function做完會被移到task queue裡,然後event loop會依序的放到stack裡執行。所以結果應該會是:
script start
script end
setTimeout
promise1
promise2
但答案是:
script start
script end
promise1
promise2
setTimeout
為什麼setTimeout會比promise1和promise2慢呢?
有興趣的可以參考 Tasks, microtasks, queues and schedules