Back to blog

Feb 09, 2025

JavaScript Execution Context - Cách mà Javascript hoạt động trên browser

PH

Phineas

@Phineas

cover

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ờ.

Đầu tiên chúng ta tìm hiểu Execution Context là gì?

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.

Các giai đoạn hoạt động của Execution Context:

  1. Creation: Ở giai đoạn này, Javascript engine sẽ tạo execution context và setup scope cho các giá trị của biến hoặc function.
  2. Execution: Trong giai đoạn này, Javascript engine sẽ thực thi code trong execution context. Nó sẽ xử lý mọi câu lệnh cũng như trạng thái của bất kỳ function nào được gọi.

Giao đoạn Creation:

MEMORYCODE
Variable: undefined Function: {…}Code sẽ được thực thi từng dòng từ trên xuống dưới

Ví dụ:

javascript
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:

  • Tạo 1 global object WINDOW (browser) global (Nodejs).
  • Setup vùng nhớ để lưu trữ biến và function.
  • Lưu trữ các biến dưới dạng undefined và function references.
MEMORYCODE
number1: undefined number2: undefined sum: {…} sum1: undefined sum2: undefined

Sau giai đoạn này, execution context sẽ chuyển sang giai đoạn thực thi code (execution phase).

Giai đoạn Execution

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.

MEMORYCODE
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
MEMORYCODE
num1: undefined num2: undefined result: undefinedCode sẽ được thực thi từng dòng từ trên xuống dưới

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.

MEMORYCODE
number1: 5 number2: 10 sum: {…} sum1: 15 sum2: 3

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:

javascript

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:

  1. Dòng 1:
javascript
console.log(5);
  • Đây là một tác vụ đồng bộ và được thực thi ngay lập tức, in ra 5.
  • Kết quả: 5
  1. Dòng 3-5:
javascript
setTimeout(function(){
    console.log(1);
});
  • setTimeout là một macrotask bất đồng bộ. Khi setTimeout được gọi, nó không thực thi ngay lập tức.
  • JavaScript sẽ chuyển hàm callback (function() { console.log(1); }) sang Web API. Sau thời gian chờ (0 milliseconds), Web API sẽ đẩy callback này vào Callback Queue.
  • Lúc này, callback vẫn đang nằm trong Callback Queue chờ đến khi Event Loop kiểm tra và đưa nó vào Call Stack để thực thi sau.
  1. Dòng 7-11:
javascript
new Promise(function(resolve, reject) {
    console.log(2);
    resolve(3);
}).then(function(value){
    console.log(value);
});
  • Khi Promise được khởi tạo, phần executor bên trong (hàm function(resolve, reject) { ... }) sẽ được thực thi ngay lập tức (đồng bộ).
  • Dòng 8: console.log(2); được thực thi và in ra 2. Kết quả: 2
  • Sau đó, resolve(3) được gọi, hứa hẹn rằng Promise đã hoàn thành. Điều này sẽ đẩy callback trong then (function(value) { console.log(value); }) vào Microtask Queue.
  • Microtasks luôn có độ ưu tiên cao hơn macrotasks, do đó chúng sẽ được xử lý ngay sau khi tất cả các tác vụ đồng bộ hoàn tất.
  1. Dòng 13: console.log(4);

Đâ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

  1. Thực thi Promise.then:

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

  1. Thực thi setTimeout:
  • Khi Microtask Queue đã trống, Event Loop sẽ kiểm tra Callback Queue để tìm các macrotasks.
  • Hàm callback của setTimeout (function() { console.log(1); }) sẽ được lấy từ Callback Queue và đưa vào Call Stack để thực thi, in ra 1. Kết quả: 1

Tổng hợp:

  • Call Stack: Chứa các tác vụ đang được thực thi đồng bộ.
  • Web API: Xử lý các tác vụ bất đồng bộ như setTimeout và setInterval. Khi hoàn thành, các callback từ Web API được chuyển đến Callback Queue.
  • Microtask Queue: Chứa các microtasks như các callback của Promise.then. Được ưu tiên xử lý trước các macrotasks.
  • Callback Queue: Chứa các macrotasks (tác vụ có độ ưu tiên thấp hơn, như setTimeout), chỉ được xử lý sau khi tất cả các microtasks đã hoàn tất.
  • Event Loop: Liên tục kiểm tra Call Stack. Nếu Call Stack trống, nó sẽ xử lý các microtasks từ Microtask Queue trước, sau đó mới đến macrotasks từ Callback Queue.

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:

Micro taskMacro task
Promise Async/await process.nextTick ….setTimeout setInterval setImmediate I/O UI render ….

Kết luận:

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.