JavaScript Under The Hood: Vòng lặp sự kiện

Dec 01 2022
Hãy làm chủ JavaScript bằng cách khám phá chức năng của nó từ đầu Bạn đã bao giờ gặp lỗi không xác định hoặc gặp khó khăn trong việc xác định phạm vi của một biến chưa? Thực sự tốn thời gian để gỡ lỗi bất kỳ lỗi nào mà không biết mã hoạt động như thế nào. Trong blog này, tôi sẽ trình bày cách các khái niệm nâng cao như vòng lặp sự kiện liên quan đến bối cảnh thực thi, ngăn xếp cuộc gọi và hàng đợi gọi lại thực sự hoạt động.

Hãy làm chủ JavaScript bằng cách khám phá chức năng của nó từ đầu

Ảnh của Marc St trên Bapt

Bạn đã bao giờ gặp lỗi không xác định hoặc gặp khó khăn trong việc xác định phạm vi của một biến chưa?

Thực sự tốn thời gian để gỡ lỗi bất kỳ lỗi nào mà không biết mã hoạt động như thế nào.

Trong blog này, tôi sẽ chứng minh các khái niệm nâng cao như event loopliên quan đến execution context, call stackcallback queuethực sự hoạt động như thế nào.

Tuyên bố từ chối trách nhiệm — Các khái niệm này cực kỳ giàu kiến ​​thức và có mối liên hệ với nhau, vì vậy đừng chớp mắt!

Công cụ JavaScript

Công cụ V8 của Google là một minh họa nổi tiếng về Công cụ JavaScript. Chẳng hạn, Chrome và Node.js đều sử dụng công cụ V8. Về cơ bản, động cơ V8 bao gồm hai phần —

  1. Ngăn xếp cuộc gọi: Tất cả mã được thực thi bên trong nó. Nó hoạt động giống như cấu trúc dữ liệu Stack, nghĩa là tuân theo khái niệm LIFO (Last In First Out).
  2. Memory Heap: Nơi diễn ra quá trình cấp phát bộ nhớ. Không giống như cấu trúc dữ liệu heap, nó chỉ là một bộ nhớ.

Bối cảnh thực thi toàn cầu hoặc GEC là Bối cảnh thực thi mặc định được tạo bởi công cụ JavaScript bất cứ khi nào nhận được tệp tập lệnh.

Tất cả mã JavaScript không nằm trong hàm được thực thi trong GEC. Các bước sau đây được thực hiện trong GEC

  • Xây dựng một không gian bộ nhớ để lưu trữ tất cả các biến và chức năng trên quy mô toàn cầu
  • Tạo một đối tượng toàn cầu.
  • Tạo từ khóathis

Tùy thuộc vào nơi mã của bạn sẽ được thực thi sẽ xác định thisvị trí của nó. Chẳng hạn, trong Node.js, nó trỏ đến một đối tượng toàn cục riêng biệt, trong khi ở trình duyệt, nó trỏ đến windowđối tượng.

Bảng điều khiển trình duyệt

Ngăn xếp cuộc gọi (Hoặc Ngăn xếp thực thi chức năng)

JavaScript là single-threaded, như bạn đã nghe nói. Nhưng nó thực sự có nghĩa là gì?

Nó có nghĩa là một công cụ JavaScript chỉ chứa một call stackhoặc function execution stack.

  • Như chúng ta biết, bất cứ khi nào trình biên dịch khám phá mã của bạn lần đầu tiên, công cụ JS sẽ được yêu cầu tạo Global Execution Contexthoặc GECbởi trình biên dịch cũng như được yêu cầu đặt nó vào tệp Call Stack.
  • Toàn bộ mã của bạn được thực thi từng cái một trong Global Execution Contextvà nó cấp phát bộ nhớ cho định nghĩa hàm hoặc khai báo biến và lưu trữ ở đó.
  • Nhưng khi bất kỳ lệnh gọi hàm nào được tìm thấy, Functional Execution Contexthoặc FECđược tạo để thực thi mã của hàm và sau đó nó được thêm vào đầu tệp call stack.
  • Trình thông dịch loại bỏ một chức năng khỏi call stackmỗi khi chức năng kết thúc. Một chức năng kết thúc - khi nó đạt đến cuối phạm vi của nó hoặc một câu lệnh trả về.
  • Cuối cùng, việc thực thi toàn bộ mã của bạn GECsẽ bị xóa khỏi tệp Call Stack.

Đừng lo lắng, hãy chứng minh điều đó bằng một ví dụ.

function f1{
  console.log('f1');
}

f1();
console.log('end');

Bước 2: — Trong ví dụ của chúng tôi, dòng đầu tiên sẽ được thực thi là f1. Một bộ nhớ sẽ được cấp phát f1và lưu trữ định nghĩa của nó.

Bước 3: — Ở dòng thứ 2, một chức năng được gọi. Đối với lời gọi hàm này, một Function Execution Context hoặc FECsẽ được tạo và nó sẽ được lưu trữ trên đầu tệp Call Stack.

Bước 4: — Bây giờ, toàn bộ f1()sẽ được thực thi từng dòng một và sau khi thực hiện xong, nó sẽ bị xóa khỏi tệp Call Stack.

Bước 5: — Sau đó, dòng cuối cùng console.log('end')sẽ được thực thi và sẽ in 'end' trên bảng điều khiển. Cuối cùng, việc thực thi toàn bộ mã của bạn, Global Execution Contextsẽ bị xóa khỏi tệp Call Stack.

JS quản lý các tác vụ không đồng bộ như thế nào?

JavaScript, như tất cả chúng ta đều biết, là một ngôn ngữ đơn luồng , đồng bộ (từng nhiệm vụ một lần) và một luồng đơn thực thi ngay lập tức bất cứ thứ gì có bên trong nó.call stack

Nhưng nếu chúng ta cần thực hiện một cái gì đó sau 5 giây thì sao? Chúng ta có thể làm điều đó bên trong call stack?

Không, chúng ta không thể. Bởi vì call stackkhông có bất kỳ bộ đếm thời gian. Nhưng làm thế nào chúng ta có thể làm điều đó?

Đây là nơi thời gian chạy JavaScript xuất hiện.

Môi trường thời gian chạy JavaScript

Có thể rất đau lòng nếu tôi nói với bạn bây giờ- setTimeout()không phải là một phần của JavaScript, mặc dù console.log()hoặc tất cả các sự kiện DOM đều là một phần của API Web cấp quyền truy cập vào Công cụ JavaScript để sử dụng tất cả các thuộc tính của nó trong Bối cảnh thực thi toàn cầu (GEC) thông qua đối tượng toàn cục window. Các API web này được gọi là không đồng bộ .

Hàng đợi gọi lại là gì?

Một hàng đợi các tác vụ được gọi là "hàng đợi gọi lại" hoặc "hàng đợi tác vụ" là một hàng đợi được thực thi sau khi hoàn thành các nhiệm vụ hiện tại của ngăn xếp cuộc gọi. Các tác vụ được đăng ký trong API web sẽ chuyển từ API web vào hàng đợi gọi lại.

Hàng đợi gọi lại hoạt động giống như cấu trúc dữ liệu hàng đợi, nghĩa là các tác vụ được xử lý theo thứ tự FIFO (vào trước, ra trước), trái ngược với ngăn xếp cuộc gọi, nghĩa là các tác vụ được xử lý theo thứ tự chúng được thêm vào hàng đợi.

Gọi lại hàng đợi

Vòng lặp sự kiện là gì?

Vòng lặp sự kiện JavaScript thêm tác vụ từ hàng đợi gọi lại vào ngăn xếp cuộc gọi theo thứ tự FIFO ngay khi ngăn xếp cuộc gọi trống.

Vòng lặp sự kiện bị chặn nếu ngăn xếp cuộc gọi hiện đang chạy một số mã và sẽ không thêm các cuộc gọi bổ sung từ hàng đợi cho đến khi ngăn xếp trống một lần nữa. Điều này là do mã JavaScript được chạy theo kiểu chạy đến khi hoàn thành.

Hãy hiểu các khái niệm trên với một ví dụ.

  • Lúc đầu, Global Execution Contextđược tạo cho mã của chúng tôi bên trong của chúng tôi call stackGECthực thi từng dòng mã của chúng tôi.
  • GECthực thi dòng đầu tiên và in 'Bắt ​​đầu' trên bảng điều khiển của chúng tôi.
  • Thực thi dòng thứ 2, setTimeout()API web sẽ được gọi và sau đó setTimeout()cấp quyền truy cập tính năng hẹn giờ. Sau đó, bạn sẽ có thể đặt 5000mslàm thời gian trễ.
  • Khi bạn chuyển callBack()chức năng qua setTimeout(), chức năng callBack()sẽ được đăng ký dưới dạng Gọi lại qua API Web web.
  • Và sau đó GECthực hiện dòng đầu tiên cũng như in 'Kết thúc' trên bảng điều khiển.
  • Khi tất cả mã được thực thi, GECsẽ bị xóa khỏi tệp call stack.
  • Sau đó 5000 millisecond, callBack()chức năng được đăng ký trong tệp web API, được chuyển vào bên trong tệp call Back Queue.
  • Event loopđặt callBack()chức năng vào call Stackkhi hoàn thành thì tất cả đều hoạt động. Và cuối cùng, callBack()chức năng được thực thi và in 'Gọi lại' vào bảng điều khiển.

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    f2();
}

main();

Nếu bạn đang nghĩ “f1” sẽ được in trước “f2” thì bạn đã nhầm. Nó sẽ là -

main
f2
f1

Cơ chế được thảo luận của JavaScript phù hợp với bất kỳ chức năng gọi lại hoặc yêu cầu API nào.

Hãy quan sát từng bước ví dụ thứ 2 hoạt động như thế nào trong môi trường thời gian chạy.

  1. Lúc đầu , mã GECsẽ được tạo bên trong call stackvà sau đó mã sẽ được thực thi từng dòng trong tệp GEC. Nó lưu trữ tất cả các định nghĩa chức năng trong Memory Heap .
  2. Khi main()được gọi, a Function Execution Context (FEC)được tạo và sau đó nó được đưa vào bên trong ngăn xếp cuộc gọi. Sau đó, tất cả mã của main()hàm sẽ được thực thi từng dòng một.
  3. Nó có một bản ghi giao diện điều khiển để in từ chính. Vì vậy, console.log('main')thực thi và đi ra khỏi ngăn xếp.
  4. setTimeout()API trình duyệt diễn ra . Chức năng gọi lại đặt nó vào hàng đợi gọi lại. Nhưng trong ngăn xếp, việc thực thi diễn ra như bình thường, vì vậy hãy f2()vào ngăn xếp. Nhật ký giao diện điều khiển của các f2()lần thực thi. Cả hai đi ra khỏi ngăn xếp.
  5. Và sau đó main()cũng bật ra khỏi ngăn xếp.
  6. Vòng lặp sự kiện nhận ra rằng ngăn xếp cuộc gọi trống và có chức năng gọi lại trong hàng đợi. Vì vậy, chức năng gọi lại f1()sẽ được đưa vào ngăn xếp bởi event loop. Thực hiện bắt đầu. Nhật ký bảng điều khiển thực thi và f1()cũng bật ra khỏi ngăn xếp. Và cuối cùng, không có gì khác trong ngăn xếp và hàng đợi để thực thi tiếp.

Đó là thực hành và lưu ý của tôi. Nếu bạn thấy nó hữu ích, vui lòng thể hiện sự ủng hộ của bạn bằng cách nhấp vào biểu tượng vỗ tay bên dưới. Bạn có thể theo dõi tôi trên phương tiện .