Event Loop in JavaScript

วันนี้มีโอกาสไปช่วยเพื่อนปั๊ปสอน NodeJS & MongoDB ระหว่างที่สอนๆ อยู่ก็พบว่ามีคำถามๆนึงที่น่าสนใจ และตอนแรกๆที่เขียน NodeJS ก็ไม่ได้เข้าใจมันเหมือนกัน เลยกะว่าคืนนี้จะเขียน Note ไว้เตือนตัวเอง และเผื่อน้องๆที่มาเทรนได้อ่านทบทวนด้วย

คำถามก็คือ ถ้า NodeJS เป็น Asynchronous แล้ว NodeJS ทำงานด้วยกี่ Thread จึงสามารถ handle concurrency ได้ และมี limitation อะไรบ้าง ซึ่ง NodeJS เนี่ยจะถูก execute ด้วย thread เพียง thread เดียว แต่ภายใน NodeJS นั้นจะมี thread อื่นๆ ที่จะมาทำงานกับ Event Loop ซึ่งจะคอยจัด task และส่ง task จาก queue เข้ามาทำงานอีกที ซึ่งโดยพื้นฐานแล้ววิธีการทำงานของ event loop นั้นจะมีรูปแบบคล้ายๆกันแบบนี้

1_xOveNFDyTmiu9ZO-DDvXdg

ในการทำงานนั้นถ้าโค้ดเป็น Synchronous เช่น

function a(){
    console.log("function a executed");
}
function b(){
    a();
    console.log("function b executed");
}
b();

ก็จะไม่มีอะไรซับซ้อน ฟังก์ชันใน call stack ก็จะเป็นระเบียบเรียบร้อยแบบนี้

a()
b()
main()

แล้วทำไมเราจึงต้องการทำอะไรให้ซับซ้อนด้วย Asynchronous ด้วยล่ะ เพราะว่าโค้ดที่เป็น Synchronous นั้นบางครั้งมัน Blocking การทำงานยังไงล่ะครับ ซึ่งมันจะทำให้การทำงานของเราช้าลง แล้ว Blocking มันเป็นหน้าตาเป็นยังไงกันล่ะ

function a() {
    $.get("//xxx.com/super_sexy_image.jpg");
}
function b() {
    $.get("//xxx.com/super_sexy_video.mp4");
}
function c() {
    console.log("I'm sexy and i know it");
}
b();
a(); // blocking โดย b()
c(); // blocking โดย b() และ a()

อาการ blocking นี้เราเจอกันบ่อยๆนะครับ เช่น กดอะไรบนหน้าเว็บก็จะไม่ active ละ กดแล้วก็รอไปจนกว่า function ที่ block จะทำงานเสร็จไปสิ ด้วยสาเหตุนี้ เราจึงใช้ callback ในการแก้ปัญหาเหล่านี้ โดย callback เหล่านี้จะไม่ได้ทำงานทันที แต่จะเก็บเอาไว้ใน Queue เช่น DOM queue สำหรับการ render (Render Queue), Network Queue สำหรับการเรียกใช้ network ต่างๆ เช่น post ไปที่ API หรือ get ข้อมูลบางอย่างจาก API และ Timer Queue สำหรับใช้ execute task ต่างๆ ตาม interval โดย Callback ทั่วๆไปก็จะมีหลักการดังนี้

โดยปกติแล้ว function ใน javascript นั้นถือว่าเป็น object ซึ่งแตกต่างจากที่เราคุ้นเคยใน Java หรือ C# หรือภาษาโปรแกรมอื่นๆ ดังนั้น เราจะสามารถสร้าง function ได้ดวยวิธีการ(แปลกๆสำหรับ Dev จากดาวดวงอื่นแบบนี้)ดังนี้

var multiply = new Function("arg1", "arg2", "return arg1 * arg2;");
หรือ
var multiply = function(a,b){ return a*b; }

ดังนั้นเราจึงสามารถใช้ function เป็น parameter สำหรับ function ได้แบบง่ายๆประมาณนี้

function random(arg1, arg2, callback) {
  var my_number = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
  callback(my_number);
}
var callback = function(num) {
    console.log("callback executed!");
    console.log("Number: " + num);
}
random(5, 15, callback);

โดยในตัวอย่างนี้ จะยังทำงานแบบ Synchronous อยู่ แต่เมื่อเราเพิ่มการทำงานแบบ Asynchronous ลงไป เช่น กำหนดให้ฟังก์ชันทำงานหลังจากสั่งไปแล้ว 5 วินาที

function random(arg1, arg2, callback) {
  var my_number = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
  callback(my_number);
}
var callback = function(num) {
    console.log("callback executed!");
    console.log("Number: " + num);
}
setTimeout(function getMeRandomNumber(){
    random(5, 15, callback)
}, 5000);

สิ่งที่เกิดขึ้นคือ setTimeout จะสั่งให้ WebApi รอ 5 วินาที จากนั้นจึงจะนำเอา function getMeRandomNumber ลงใน task queue แล้วรอให้ call stack ว่าง event loop จึงจะนำ function getMeRandomNumber มา execute บน call stack

1_uKQRHgrqt-W3J8PAQTCJtA

setTimeOut -> Web Apis

1_0EOdOKAHyKi5c-UZBGaeLg

Web Apis เก็บ task ลงใน task queue

1_V6WK3xQbkQOO15ZD3Br-Ag

เมื่อ call stack ว่าง event loop จึงดึงเอา task queue ขึ้นมาทำงาน
จะเห็นได้ว่า setTimeOut หรือคำสั่งเกี่ยวกับ Timer นั้นไม่ได้รับประกันว่า function จะทำงานภายในระยะเวลาที่กำหนดเป๊ะๆ แต่จะทำงานด้วยระยะเวลาขั้นต่ำ ภายในที่กำหนด ซึ่งมันจะทำงานได้จริงๆก็ต่อเมื่อ call stack ว่างแล้วเท่านั้น

ซึ่งถ้าเราเข้าใจการทำงานของ Event Loop แล้วจะทำให้เราเข้าใจ Asyncronous operation ใน JavaScript ได้ไม่ยากเลย

สำหรับใครที่ภาษาอังกฤษแข็งแรง แนะนำให้ดูวีดีโอนี้ครับ เพราะอธิบายได้แจ่มแมวมาก

และ Phillip Roberts ยังได้แชร์เครื่องมือสำหรับ visualize callstack ใน JavaScript ให้ลองใช้กันด้วย สามารถไปใช้ได้ที่ URL นี้เลยครับ

Reference: