Feb 09, 2025
Phineas
@Phineas
JavaScript là một ngôn ngữ thông dịch đơn luồng và là một ngôn ngữ lập trình phổ biến được sử dụng chủ yếu trong phát triển web để tạo ra các trang web động và tương tác.
Mỗi browser đều có một công cụ JavaScript khác nhau để xử lý và thực thi mã JavaScript. Google Chrome thì có V8 engine, Mozilla Firefox sử dụng SpiderMonkey, và các trình duyệt khác cũng có công cụ riêng. Và mục tiêu của tất cả các công cụ này là giống nhau, vì các trình duyệt không thể hiểu trực tiếp mã JavaScript. Để hiểu browser hiểu được thì javascript sẽ thực thi rất nhiều thứ. Hãy tìm hiểu nó ngay bây giờ.
Khi JS engine scan được tệp JS, nó sẽ tạo ra 1 enviroment được gọi là Execution Context để xử lý các đoạn code.
Trong quá trình runtime, chúng sẽ phân tích mã nguồn và đồng thời cấp phát bộ nhớ cho các biến cũng như hàm. Sau đó thực thi.
Ví dụ:
var number1 = 5;
var number2 = 10;
function sum(num1, num2) {
var result = num1 + num2;
return result;
}
var sum1 = sum(number1, number2);
var sum2 = sum(1,2);
console.log(sum1);
console.log(sum2);
Khi bắt đầu, JS engine sẽ thực thi đoạn code, nó sẽ tạo global execution context sau đó sẽ thực thi như sau:
Sau giai đoạn này, execution context sẽ chuyển sang giai đoạn thực thi code (execution phase).
Bây giờ, nó bắt đầu đi qua toàn bộ dòng code của chúng ta, từ trên xuống dưới. Ngay khi gặp number1 = 5, nó sẽ gán giá trị 5 cho number1 trong bộ nhớ. Tiếp tục như thế đến number 2 cũng tương tự. Sau đó chúng đến hàm sum, vì sum lúc này được cấp phát trong bộ nhớ, nên chúng ta sẽ nhảy đến dòng var sum1 = sum(number1, number2).
Hàm sum sẽ được gọi và Javascript một lần nữa tạo mới 1 function execution context.
var sum1 = sum(number1,number2);
Sau khi tính toán xong, giá trị sẽ được gán cho result và được trả ra gán cho biến sum1. Điều này cũng tương tự với sum2. Và lưu ý, sau khi tính toán xong thì function execution context cũng sẽ bị destroyed.
OK. Vậy làm sao JS engine nó có thể theo dõi được toàn bộ các context. Chúng ta sẽ tiếp tục tìm hiểu Call Stack.
Trong JS, nơi cấp phát bộ nhớ chúng ta gọi là heap. Và nơi có thể theo dõi toàn bộ context, bao gồm cá global và functional context chúng ta gọi là call stack.
Call stack hay stack giúp theo dõi chúng ta đang ở execution context nào - nghĩa là function nào đang được thực thi. Và giúp ta xác định sau khi hoàn thành function đó thì sẽ trở về nơi nào để tiếp tục thực thi chương trình. Nó hoạt động theo cơ chế LIFO (LAST-IN-FIRST-OUT).
Khi JS engine bắt đầu thực thi, nó sẽ tạo 1 global context và push nó vào trong stack. Và bất kể khi nào function được gọi, JS engine cũng sẽ tạo 1 function stack context cho function, push vào đầu stack và bắt đầu thực thi nó.
Khi thực thi xong nó cũng sẽ được tự động xoá ra khỏi stack.
Chi tiết hoá hơn nữa. Chúng ta tìm hiểu xem call stack sẽ hoạt động như nào. Dưới đây là hình mô tả cách hoạt động
Vậy hình trên có ý nghĩa gì. Ta có 1 ví dụ như sau:
Trong 1 trang web A có nhiều component trên 1 màn hình, trong đó 1 component chứa hình ảnh. Nếu khi chúng ta load trang web lên, vì nếu phải đợi hình ảnh render (load xong) thì mới render cả trang web ra thì thực sự đó là 1 trải nghiệm khá tệ. Vì vậy, các nhà phát triển đã cho ra đời VÒNG LẶP SỰ KIỆN (event loop). Chúng giúp xử lý 2 việc chính, tác vụ đồng bộ, và bất đồng bộ.
Chúng ta có 1 ví dụ như sau:
console.log(5);
setTimeout(function(){
console.log(1);
});
new Promise(function(resolve, reject) {
console.log(2);
resolve(3);
}).then(function(value){
console.log(value);
})
console.log(4);
Chúng ta có thể thấy output là 5 2 4 3 1. Giải thích cho kết quả bằng hình vẽ bên trên.
Phân tích kết quả từng bước:
console.log(5);
setTimeout(function(){
console.log(1);
});
new Promise(function(resolve, reject) {
console.log(2);
resolve(3);
}).then(function(value){
console.log(value);
});
Đây là một tác vụ đồng bộ, được thực thi ngay lập tức sau các tác vụ đồng bộ trước đó. Kết quả: 4
Sau khi tất cả các tác vụ đồng bộ đã hoàn tất, Event Loop sẽ kiểm tra và thực thi các microtasks trước. Do đó, callback của Promise.then (function(value) { console.log(value); }) sẽ được thực thi, in ra 3. Kết quả: 3
Tổng hợp:
Lưu ý:
Cùng là tác vụ bất đồng bộ nhưng các tác vụ macrotask sẽ do WebAPI phụ trách xử lý, nhưng microtask lại không.
Micro task và Macro task:
Tóm lại, Execution context trong JavaScript là một phần quan trọng để hiểu cách JavaScript hoạt động đằng sau hậu trường. Nó xác định môi trường trong đó mã được thực thi cũng như những biến và hàm nào có sẵn để sử dụng.
Hy vọng bài viết của mình hữu ích đến các bạn. Cảm ơn các bạn đã đọc đến đây.
MEMORY | CODE |
number1: 5 number2: 10 sum: {…} sum1: 15 sum2: 3 |
MEMORY | CODE |
number1: undefined number2: undefined sum: {…} sum1: undefined sum2: undefined | Đến dòng tính var sum1 = sum(num1, num2) sẽ tạo tiếp function execution |
Micro task | Macro task |
Promise Async/await process.nextTick …. | setTimeout setInterval setImmediate I/O UI render …. |
MEMORY | CODE |
number1: undefined number2: undefined sum: {…} sum1: undefined sum2: undefined |
MEMORY | CODE |
num1: undefined num2: undefined result: undefined | Code sẽ được thực thi từng dòng từ trên xuống dưới |
MEMORY | CODE |
Variable: undefined Function: {…} | Code sẽ được thực thi từng dòng từ trên xuống dưới |