JavaScript Under The Hood

Nov 28 2022
Mục lục Trong bài viết này, chúng ta sẽ đi sâu vào hoạt động bên trong của JavaScript và cách nó thực sự chạy. Bằng cách hiểu các chi tiết, bạn sẽ hiểu hành vi của mã của mình và do đó bạn có thể viết các ứng dụng tốt hơn.

Mục lục

  • Chủ đề và ngăn xếp cuộc gọi
  • Bối cảnh thực hiện
  • Vòng lặp sự kiện và JavaScript không đồng bộ
  • Bộ nhớ lưu trữ và thu gom rác
  • Biên dịch JIT (Just In Time)
  • Bản tóm tắt

Trong bài viết này, chúng ta sẽ đi sâu vào hoạt động bên trong của JavaScript và cách nó thực sự chạy. Bằng cách hiểu các chi tiết, bạn sẽ hiểu hành vi của mã của mình và do đó bạn có thể viết các ứng dụng tốt hơn.

JavaScript được mô tả là:

Ngôn ngữ lập trình được biên dịch đơn luồng, thu gom rác, thông dịch hoặc Just In Time với vòng lặp sự kiện không chặn.

Hãy giải nén từng thuật ngữ chính này.

Chủ đề & ngăn xếp cuộc gọi:

Công cụ JavaScript là một trình thông dịch đơn luồng bao gồm một đống và một ngăn xếp cuộc gọi duy nhất được sử dụng để thực thi chương trình.
Ngăn xếp cuộc gọi là một cấu trúc dữ liệu sử dụng nguyên tắc Nhập sau, Xuất trước (LIFO) để tạm thời lưu trữ và quản lý lời gọi hàm (gọi).
Điều đó có nghĩa là hàm cuối cùng được đẩy vào ngăn xếp sẽ là hàm đầu tiên được bật ra khi hàm trả về.
Vì ngăn xếp cuộc gọi là một, nên việc thực thi (các) chức năng được thực hiện lần lượt, từ trên xuống dưới. Nó có nghĩa là ngăn xếp cuộc gọi là đồng bộ.

Bây giờ, vì nó đồng bộ, bạn sẽ tự hỏi làm thế nào JavaScript có thể xử lý các cuộc gọi không đồng bộ?
Chà, vòng lặp sự kiện là bí mật đằng sau lập trình không đồng bộ của JavaScript.
Nhưng trước khi giải thích khái niệm lệnh gọi không đồng bộ trong JavaScript và cách thực hiện điều đó với ngôn ngữ đơn luồng, trước tiên chúng ta hãy hiểu cách thực thi mã.

Bối cảnh thực thi (EC):

Bối cảnh thực thi được định nghĩa là môi trường trong đó mã JavaScript được thực thi.
Việc tạo Bối cảnh thực thi xảy ra theo hai giai đoạn:

1. Giai đoạn tạo ký ức:

  • Tạo đối tượng toàn cục (được gọi là đối tượng cửa sổ trong trình duyệt và đối tượng toàn cầu trong NodeJS).
  • Tạo đối tượng “this” và liên kết nó với đối tượng toàn cục.
  • Thiết lập heap bộ nhớ (A heap là một vùng bộ nhớ lớn, hầu như không có cấu trúc) để lưu trữ các tham chiếu biến và hàm.
  • Lưu trữ các hàm và biến trong bối cảnh thực thi toàn cầu bằng cách triển khai Hoisting .

Bây giờ chúng ta đã biết các bước đằng sau việc thực thi mã, hãy quay lại

Vòng lặp sự kiện:

Trước tiên, hãy bắt đầu bằng cách xem sơ đồ này:

Vòng lặp sự kiện trong JS

Chúng tôi có công cụ bao gồm hai thành phần chính:
* Memory Heap — đây là nơi diễn ra quá trình cấp phát bộ nhớ.
* Ngăn xếp cuộc gọi — đây là nơi chứa các khung ngăn xếp khi mã của bạn thực thi.

Chúng tôi có các API Web là các chuỗi mà bạn không thể truy cập, bạn chỉ có thể gọi chúng. Chúng là các phần của trình duyệt trong đó khởi động đồng thời, như DOM, AJAX, setTimeout, v.v.

Cuối cùng, có hàng đợi Gọi lại là danh sách các sự kiện sẽ được xử lý. Mỗi sự kiện có một chức năng liên quan được gọi để xử lý nó.

Vậy nhiệm vụ của vòng lặp sự kiện ở đây là gì?
Event Loop có một công việc đơn giản — theo dõi Call Stack và Callback Queue. Nếu Ngăn xếp cuộc gọi trống, Vòng lặp sự kiện sẽ lấy sự kiện đầu tiên từ hàng đợi và sẽ đẩy nó vào Ngăn xếp cuộc gọi, thứ sẽ chạy nó một cách hiệu quả.
Một lần lặp lại như vậy được gọi là đánh dấu trong Vòng lặp sự kiện. Mỗi sự kiện chỉ là một hàm gọi lại.

Lưu trữ bộ nhớ và thu gom rác:

Để hiểu nhu cầu thu gom rác, trước tiên chúng ta phải hiểu Vòng đời bộ nhớ, điều này gần như giống nhau đối với bất kỳ ngôn ngữ lập trình nào, nó có 3 bước chính.
1. Cấp phát bộ nhớ.
2. Sử dụng bộ nhớ được cấp phát để đọc hoặc ghi hoặc cả hai.
3. Giải phóng bộ nhớ đã cấp phát khi không còn cần thiết.

Phần lớn các sự cố quản lý bộ nhớ xảy ra khi chúng tôi cố gắng giải phóng bộ nhớ được cấp phát. Mối quan tâm chính phát sinh là việc xác định tài nguyên bộ nhớ không sử dụng.
Trong trường hợp các ngôn ngữ cấp thấp mà nhà phát triển phải tự quyết định khi nào bộ nhớ không còn cần thiết, các ngôn ngữ cấp cao như JavaScript sử dụng một hình thức quản lý bộ nhớ tự động được gọi là Bộ sưu tập rác (GC).
JavaScript sử dụng hai chiến lược nổi tiếng để thực hiện GC: kỹ thuật đếm tham chiếu và thuật toán đánh dấu và quét.
Đây là giải thích chi tiết từ MDN về cả hai thuật toán và cách chúng hoạt động.

JIT (Just In Time) Biên soạn:

Hãy quay lại định nghĩa JavaScript: Nó nói “Ngôn ngữ lập trình được biên dịch bởi JIT”, vậy điều đó có nghĩa là gì? Còn về việc bắt đầu với sự khác biệt giữa trình biên dịch và trình thông dịch nói chung thì sao?

Tương tự như vậy, hãy nghĩ về hai người có ngôn ngữ khác nhau muốn giao tiếp. Biên dịch giống như việc dừng lại và dành toàn bộ thời gian để học ngôn ngữ, còn phiên dịch sẽ giống như có người ở đó để phiên dịch từng câu.

Vì vậy, các ngôn ngữ được biên dịch có thời gian ghi chậm và thời gian chạy nhanh và các ngôn ngữ được giải thích thì ngược lại.

Nói với các thuật ngữ kỹ thuật: biên dịch là một quá trình chuyển đổi mã nguồn chương trình thành mã nhị phân mà máy có thể đọc được, trước khi thực thi và trình biên dịch sẽ xử lý toàn bộ chương trình trong một lần.

Mặt khác, trình thông dịch là một chương trình thực thi các hướng dẫn của chương trình mà không yêu cầu chúng phải được biên dịch trước thành định dạng mà máy có thể đọc được và mỗi lần nó nhận một dòng mã.

Và ở đây có vai trò biên dịch JIT đang cải thiện hiệu suất của các chương trình được giải thích. Toàn bộ mã được chuyển đổi thành mã máy ngay lập tức và sau đó được thực thi ngay lập tức .

Bên trong trình biên dịch JIT, chúng ta có một thành phần mới được gọi là màn hình (hay còn gọi là trình biên dịch). Màn hình đó xem mã khi nó chạy và

  • Xác định các thành phần nóng hoặc ấm của mã, ví dụ: mã lặp đi lặp lại.
  • Chuyển các thành phần đó thành mã máy trong thời gian chạy.
  • Tối ưu hóa mã máy được tạo.
  • Trao đổi nóng việc triển khai mã trước đó.

Bây giờ chúng ta đã hiểu các khái niệm cốt lõi, hãy dành một phút để kết hợp mọi thứ lại với nhau và tóm tắt các bước mà JS Engine tuân theo khi thực thi mã:

Nguồn hình ảnh: kênh truyền thông traversy
  1. Công cụ JS lấy mã JS được viết theo cú pháp mà con người có thể đọc được và biến nó thành mã máy.
  2. Công cụ sử dụng trình phân tích cú pháp để đi qua từng dòng mã và kiểm tra xem cú pháp có đúng không. Nếu có bất kỳ lỗi nào, mã sẽ ngừng thực thi và một lỗi sẽ được đưa ra.
  3. Nếu tất cả các kiểm tra vượt qua, trình phân tích cú pháp sẽ tạo cấu trúc dữ liệu dạng cây được gọi là Cây cú pháp trừu tượng (AST).
  4. AST là một cấu trúc dữ liệu đại diện cho mã theo cấu trúc dạng cây. Việc chuyển mã thành mã máy từ AST sẽ dễ dàng hơn.
  5. Sau đó, trình thông dịch tiếp tục lấy AST và biến nó thành IR, đây là sự trừu tượng hóa của mã máy và là trung gian giữa mã JS và mã máy. IR cũng cho phép thực hiện tối ưu hóa và di động hơn.
  6. Sau đó, trình biên dịch JIT lấy IR được tạo ra và biến nó thành mã máy, bằng cách biên dịch mã, nhận phản hồi nhanh chóng và sử dụng phản hồi đó để cải thiện quy trình biên dịch.

Cảm ơn bạn đã đọc :)

Bạn có thể theo dõi tôi trên Twitter và LinkedIn .