+6

Event Loop - Cách Javascript runtime hoạt động

Khi tìm hiểu về Javascript, ta thường nghe đến một số thuật ngữ như Engine V8, call stack, single-threaded, non-blocking, asynchronous, concurrent, .... Vậy những thuật ngữ này mang ý nghĩa gì? và cách Javascript hoạt động như thế nào thì ta cùng tìm hiểu ở bài viết này nhé.

Engine V8

Engine V8 được phát triển bởi Google và viết bằng C++. V8 được sử dụng bởi Chrome và Chromium. Nhờ có sự ra đời của NodeJS mà cái tên V8 đã trở nên rất phổ biến.

V8 Engine có hai thành phần chính đó là: memory heapcall stack.

  • Memory Heap: để cấp phát bộ nhớ
  • Call Stack: cấu trúc dữ liệu chưa các lời gọi hàm mà code Javascript thực thi

Trong code base của V8 Engine, ta sẽ không thể tìm thấy một số thứ ta vẫn hay dùng trong Javascript như setTimeout, DOM hoặc HTTP request. Vì những thứ này không phải của V8 mà chúng thuộc về Web APIs cung cấp bởi browser.

Call Stack

Javascript là một ngôn ngữ đơn luồng (single-threaded) nên chỉ có một Call Stack và chỉ có thể thực thi một công việc tại một thời iđiểm.

Trong dev tool của Chrome ta có thể xem Call Stack trong mục Source. Cụ thể khi ta thực thi một hàm trong Javascript thì hàm đó sẽ được đẩy (push) vào trong Call Stack và các câu lệnh cũng được đẩy vào theo cấu trúc dữ liệu stack. Khi các câu lệnh hay hàm được thực thi xong chúng sẽ ra khỏi (pop) Call Stack.

Để dễ có thể hình dung cách mà Call Stack hoạt động các bạn có thể tham khảo ở đây.

function foo() {
    console.log('Hello World');
}

function sayHello() {
    foo();

    console.log('Done');
}

sayHello();

Ta có thể thấy khi hàm sayHello() được gọi sẽ được đẩy vào Call Stack. VÌ trong hàm sayHello() có gọi đến hàm foo() nên hàm foo() tiếp tục được đẩy vào, tiếp theo t có console.log('Hello World') của hàm foo(). Cứ tiếp diễn như thế code sẽ được thực thi đến khi trong Call Stack rỗng thì thôi.

Khi đoạn code trên được thực thi ta sẽ có màn console như sau.

Hello World
Done

Khi log ra một exception, ta có thể thấy một stack trace cũng được dựa trên Call Stack.

function fail() {
    throw new Error('Error');
}

function sayHello() {
    fail();
}

sayHello();

Hay lỗi Stack overlow xảy ra khi chương trình vượt quá kích thước của Call Stack.

function sayHello() {
    sayHello();
}

sayHello();

Khi mọi thứ trong Call Stack trở nên châm hơn hay thực thi lâu hơn thì sẽ như thế nào? Các ví dụ trên chỉ đơn giản là log ra console nên code được thực thi rất nhanh nếu logic là các synchronous request thì ta sẽ phải đợi thực thi lần lượt các request.

let items1 = $.sync('foo1.com') // 30 seconds
let items2 = $.sync('foo2.com') // 1 minutes
let items3 = $.sync('foo3.com') // 2 minutes

console.log(items1);
console.log(items2);
console.log(items3);

Ở ví dụ trên khi console.log(items1) được cho vào Call Stack ta sẽ phải đợi 30 giây rồi tiếp theo là 1 phút và 2 phút, chẳng may khi network có vấn đề thì ta sẽ phải đợi lâu hơn nữa. Đây là một vấn đề lớn khi ta thực thi code trên trình duyệt. Khi có các synchronous request ta sẽ không thể làm gì khác với trình duyệt như click button để hiển thị dialog mà ta phải đợi vì nó không thể chạy các logic code khác hay không thể render nên sẽ ảnh hưởng rất nhiều đến người dùng. Để khắc phục vấn đề này ta sẽ phải dùng đến non-blocking function trong trình duyệt là Asynchronous callback.

Tiếp theo ta có một ví dụ khác.

function foo() {
    console.log('Hello World');
}

function sayHello() {
    foo();

    console.log('Done');
}

sayHello();

setTimeout(function() {
    console.log('Asynchronous')
}, 2000)

console.log('Final');

Màn console ta có.

Hello World
Done
Final
Asynchronous

Tại sao lại như vậy? Nếu theo Call Stack ta vừa tìm hiểu ở trên thì hàm setTimeout sẽ được cho vào Call Stack trước và câu lệnh console.log('Asynchronous') phải được thực thi trước thì Asynchronous phải được log ra rồi sau đó đến Final chứ tại sao Final lại được log ra console trước? Đó là vì hàm setTimeout là một asynchronous (bất đồng bộ) và để tìm hiểu xem cách thực thi của chúng ta cùng tìm hiểu đến phần bên dưới.

Javascript Runtime

Như ta đã đề cập ở trên là mốt số API như AJAX, DOM hay setTimeout, ... không thuộc về JS Engine mà nó là của Web APIs nên Javascript Runtime phức tạp hơn nhiều chứ không gói gọn trong JS Engine. Ở bài viết này ta sẽ tìm hiểu JS runtime trong môi trường browser. Nó bao gồm các thành phần như sau.

Trong hình ta có thể thấy 2 thành phần đó là Event LoopCallback Queue được cung cấp bởi trình duyệt và chúng chạy trên thread riêng và được đảm bảo tính concurrency. Để hiểu rõ hơn về hai thuật ngữ này ta sẽ đi tiếp đến phần bên dưới.

Event Loop và Callback queue

Callback queue là cấu trúc dữ liệu chứa các hàm async callback.

Event Loop có một nhiệm vụ rất đơn giản đó là quan sát Call StackCallback Queue nếu Call Stack rỗng thì sẽ lấy hàm đầu tiên trong queue và đẩy nó vào trong Call Stack để thực thi.

Để hiểu rõ hơn về hai khái niệm này ta cùng xét ví dụ bên dưới.

function main() {
    console.log('Start');

    setTimeout(function() {
        console.log('Callback');
    }, 2000)
    
    console.log('Done');
}

main();

Khi hàm main() được gọi nó sẽ được đẩy vào Call Stack. Tiếp đến câu lệnh console.log('Start') cũng được đẩy vào.

Sau khi đã thực hiện xong câu lệnh console.log('Start') thì tiếp đến hàm setTimeout(function()).

Hàm setTimeout(function()) được cung cấp bới Web APIs nên nó sẽ được chuyển sang Web APIstimer để trình duyệt tính toán thời gian chờ thực thi hàm.

Tiếp đến câu lệnh console.log('Done') được chuyển vào Call Stack. Và Web APIs đã timer xong thì hàm thực thi sẽ được chuyển vào Callback Queue để chờ.

Sau khi thực thi xong console.log('Done') thì hàm main() cũng sẽ được thực thi xong nên được đưa ra khỏi stack. Lúc này Event Loop quan sát thấy Call Stack đang trống nên sẽ đưa anonymous function trong setTimeout vào Call Stack để Engine V8 tiến hành thực thi. Console lúc này ta sẽ có.

Start
Done
Callback

Vây là mình giải thích về quá trình đơn giản của Javascript Runtime. Bây giờ mình sẽ tiến hành sửa một chút cho hàm setTimeout về thời gian chờ thực thi là 0 thì liệu hàm trong đó có được thực thi hay luôn không?.

function main() {
    console.log('Start');

    setTimeout(function() {
        console.log('Callback');
    }, 0)
    
    console.log('Done');
}

main();

Nếu các bạn thử sửa và tiến hành chạy code thì sẽ thấy kết quả không hề thay đổi. Điều này lại càng khẳng định cho cách hoạt động của Event Loop. Ngoài setTimeout thì các API khác cũng đều hoạt động như vậy với async callback.

Tổng kết

Qua bài viết này mình và các bạn đã cùng tìm hiểu về Event Loop trong Javascript. Những kiến thức trên cũng chỉ là do mình tự tìm hiểu và học từ người khác nếu có sai sót gì mong mọi người có thể góp ý để bài viết hoàn thiện hơn. Hy vọng qua bài viết này các bạn có thể hiểu hơn về cách mà Javascript hoạt động. Cảm ơn các bạn đã theo dõi đến hết bài viết ❤️.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.