Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Node.js & npm

Trong bài học này, chúng ta sẽ xem xét động cơ JavaScript và giải phẫu của nó. Chúng tôi cũng sẽ tìm hiểu về ngăn xếp cuộc gọi của JavaScript, vòng lặp sự kiện, hàng đợi nhiệm vụ và nhiều phần khác làm cho JavaScript như chúng tôi biết nó hoạt động đúng.

Bạn đã bao giờ tự hỏi mình, làm thế nào tất cả điều này hoạt động đằng sau hậu trường? Tôi biết tôi có.

Có một sự hiểu biết sâu sắc về các khái niệm nhất định cho phép chúng ta hiểu mã theo cách tốt hơn nhiều, thực hiện tốt hơn trong công việc của chúng ta và ngoài ra, nó rất hữu ích trong các cuộc phỏng vấn việc làm.

Và nó cũng có thể là một chủ đề siêu thú vị để học hỏi, vì vậy với điều đó đã được nói, hãy để Lôi đi vào các chi tiết về cách thức hoạt động của động cơ JS.

Trong bài đăng này, chúng tôi sẽ đi sâu vào thế giới của JS, cách nó hoạt động đằng sau hậu trường, từ động cơ, đến các khái niệm như nâng cao, bối cảnh thực thi, môi trường từ vựng và hơn thế nữa.

Tại đây, một tổng quan chung về cách thức hoạt động của động cơ JS:

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Don Tiết lo lắng nếu bạn không hiểu bất cứ điều gì trong số này, vào cuối bài viết, bạn sẽ hiểu từng bước của sơ đồ này.

Đi nào!

Môi trường

Một máy tính, trình biên dịch hoặc thậm chí là một trình duyệt có thể thực sự hiểu về mã mà viết mà viết trong JS. Nếu vậy, mã chạy như thế nào?

Đằng sau hậu trường, JS luôn chạy trong một môi trường nhất định, hầu hết những người phổ biến là:

  1. Trình duyệt (cho đến nay là phổ biến nhất)
  2. Node.js (là môi trường thời gian chạy cho phép bạn chạy JS bên ngoài trình duyệt, thường là trong máy chủ)

Động cơ

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Vì vậy, JS cần phải chạy trong một môi trường nhất định, nhưng chính xác là gì trong môi trường?

Khi bạn viết mã trong JS, bạn viết nó theo cú pháp có thể đọc được của con người, với bảng chữ cái và số. Như đã đề cập, một máy không thể hiểu loại mã này.

Đây là lý do tại sao mỗi môi trường có một động cơ.

Nói chung, công việc của động cơ là lấy mã đó và chuyển đổi nó thành mã máy cuối cùng có thể được chạy bởi bộ xử lý máy tính.machine code which can eventually be run by the computer processor.

Mỗi môi trường có động cơ riêng, những môi trường phổ biến nhất là Chrome V8 (nút cũng sử dụng), Firefox Spidermonkey, JavaScriptcore của Safari và Chakra bởi IE.

Tất cả các động cơ hoạt động theo cách tương tự nhưng có sự khác biệt giữa mỗi động cơ.

Nó cũng rất quan trọng để ghi nhớ rằng đằng sau hậu trường, một động cơ chỉ đơn giản là một phần mềm, Chrome V8 chẳng hạn là một phần mềm được viết bằng C ++.

Trình phân tích cú pháp

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Vì vậy, chúng tôi có một môi trường, và chúng tôi có một động cơ bên trong môi trường đó. Điều đầu tiên động cơ thực hiện khi thực thi mã của bạn là kiểm tra mã bằng trình phân tích cú pháp.

Trình phân tích cú pháp biết cú pháp và quy tắc của JS, và công việc của nó là đi qua từng dòng mã và kiểm tra xem cú pháp của mã có chính xác không.

Nếu trình phân tích cú pháp gặp phải lỗi, nó sẽ ngừng chạy và gửi lỗi. Nếu mã hợp lệ, trình phân tích cú pháp sẽ tạo ra một cái gì đó được gọi là cây cú pháp trừu tượng (hoặc viết tắt là AST).Abstract Syntax Tree (or AST for short).

Cây cú pháp trừu tượng (AST)

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Vì vậy, môi trường của chúng tôi có một động cơ, có trình phân tích cú pháp, tạo ra AST. Nhưng AST là gì và tại sao chúng ta cần nó?

AST là một cấu trúc dữ liệu, không phải là duy nhất đối với JS nhưng thực sự được sử dụng bởi rất nhiều ngôn ngữ khác (một số trong số chúng là Java, C#, Ruby, Python).

AST chỉ đơn giản là một biểu diễn cây của mã của bạn và lý do chính khiến động cơ tạo ra AST thay vì biên dịch trực tiếp với mã máy là vì nó dễ dàng chuyển đổi thành mã máy hơn khi bạn có mã bên trong cấu trúc dữ liệu cây.

Bạn thực sự có thể xem AST trông như thế nào, chỉ cần đặt bất kỳ mã nào vào trang web Astexplorer và kiểm tra cấu trúc dữ liệu mà Lừa tạo ra:ASTExplorer and check out the data structure that’s created:

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Thông dịch viên

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Công việc của người phiên dịch là lấy AST đã được tạo và biến nó thành một đại diện trung gian của mã (IR).Intermediate Representation of the code (IR).

Chúng tôi sẽ tìm hiểu thêm về thông dịch viên sau này, vì bối cảnh tiếp theo được yêu cầu để hiểu đầy đủ nó là gì.

Đại diện trung gian (IR)

Vậy IR này là gì mà thông dịch tạo từ AST là gì?

IR là cấu trúc dữ liệu hoặc mã đại diện cho mã nguồn. Vai trò của nó là là một bước trung gian giữa mã mà viết bằng ngôn ngữ trừu tượng như JS và mã máy.

Về cơ bản bạn có thể nghĩ về IR như một sự trừu tượng của mã máy.

Có nhiều loại IR, một động cơ JS rất phổ biến là mã byte. Ở đây, một hình ảnh thể hiện vai trò IR trong động cơ V8:

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Nhưng bạn có thể hỏi tại sao chúng ta cần phải có IR? Tại sao không chỉ biên dịch thẳng vào mã máy. Có 2 lý do chính tại sao các công cụ sử dụng IR làm bước trung gian giữa mã cấp cao và mã máy:

  1. Tính di động - Khi mã được biên dịch cho mã máy, nó cần phải khớp với phần cứng mà nó chạy trên. — when code gets compiled to machine code, it needs to match the hardware that it’s run on.

Mã máy viết cho bộ xử lý Intel và mã máy được viết cho bộ xử lý ARM sẽ khác. Một IR, mặt khác, là phổ quát và có thể phù hợp với bất kỳ nền tảng nào. Điều này làm cho quá trình chuyển đổi sau dễ dàng và di động hơn.

  1. Tối ưu hóa - Nó dễ dàng hơn để chạy tối ưu hóa với IR so với mã máy, điều này đúng cả từ quan điểm tối ưu hóa mã và tối ưu hóa phần cứng. — it’s easier to run optimizations with IR compared to machine code, this is true both from code optimizations point of view and hardware optimizations.

Sự thật thú vị: Các động cơ JS không phải là những người duy nhất sử dụng mã byte như IR, trong số các ngôn ngữ cũng sử dụng mã byte mà bạn sẽ tìm thấy C#, Ruby, Java, v.v.

Trình biên dịch

Hướng dẫn how does javascript and javascript engine work? - javascript và javascript engine hoạt động như thế nào?

Công việc của trình biên dịch là lấy IR mà trình thông dịch tạo, trong trường hợp của chúng tôi Bytecode và chuyển đổi nó thành mã máy với một số tối ưu hóa nhất định.

Hãy nói về việc tổng hợp mã và một số khái niệm cơ bản. Hãy nhớ rằng đây là một chủ đề lớn mất nhiều thời gian để làm chủ, vì vậy tôi sẽ chỉ chạm vào nó nói chung cho trường hợp sử dụng của chúng tôi.

Phiên dịch vs trình biên dịch

Có hai cách để dịch mã thành một cái gì đó mà máy có thể chạy, sử dụng trình biên dịch và sử dụng trình thông dịch.

Sự khác biệt giữa trình thông dịch và trình biên dịch là trình thông dịch dịch mã của bạn và thực thi từng dòng trong khi trình biên dịch ngay lập tức dịch tất cả mã thành mã máy trước khi thực thi nó.

Có những ưu và nhược điểm cho mỗi người, một trình biên dịch rất nhanh nhưng phức tạp và chậm để bắt đầu, một thông dịch viên chậm hơn nhưng đơn giản hơn.

Với điều đó đã được nói, có 3 cách để biến mã cấp cao thành mã máy và chạy nó:

  1. Giải thích - Với chiến lược này, bạn có một trình thông dịch đi qua dòng mã từng dòng và thực thi nó (không quá hiệu quả). — with this strategy you have an Interpreter which goes through the code line by line and executes it (not so efficient).
  2. Trước khi biên dịch (AOT) - Ở đây bạn có một trình biên dịch trước tiên biên dịch toàn bộ mã và chỉ sau đó thực thi nó. — here you have a compiler first compiling the entire code, and only then executing it.
  3. Việc tổng hợp thời gian-kết hợp giữa chiến lược AOT và chiến lược giải thích, chiến lược tổng hợp JIT cố gắng sử dụng tốt nhất từ ​​cả hai thế giới, thực hiện biên dịch động, nhưng cũng cho phép một số tối ưu hóa nhất định xảy ra, điều này thực sự tăng tốc quá trình biên dịch. Chúng tôi sẽ giải thích thêm về tổng hợp JIT. — Combination between the AOT strategy and the interpretation strategy, a JIT compilation strategy attempts to take the best from both worlds, performing dynamic compilation, but also allowing certain optimizations to happen, which really speeds up the compilation process. We’ll explain more about JIT compilation.

Hầu hết các động cơ JS sử dụng trình biên dịch JIT nhưng không phải tất cả chúng. Ví dụ, Hermes, động cơ phản ứng bản địa sử dụng, không sử dụng trình biên dịch JIT.doesn’t use a JIT compiler.

Để tóm tắt, trong các động cơ JS, trình biên dịch lấy IR được tạo bởi trình thông dịch và tạo mã máy được tối ưu hóa từ nó.

Trình biên dịch JIT

Như chúng tôi đã nói, hầu hết các động cơ JS đều sử dụng phương pháp biên dịch JIT. JIT kết hợp cả chiến lược và giải thích AOT, cho phép một số tối ưu hóa nhất định xảy ra. Hãy cùng đi sâu hơn vào các tối ưu hóa này và chính xác trình biên dịch làm gì.

Tối ưu hóa tổng hợp JIT được thực hiện bằng cách lấy mã mà lặp lại và tối ưu hóa nó. Quá trình tối ưu hóa hoạt động như sau:

Về bản chất, trình biên dịch JIT nhận được phản hồi bằng cách thu thập dữ liệu định hình cho mã được thực thi, nếu nó xuất hiện bất kỳ phân đoạn mã nóng nào (mã lặp lại), phân đoạn nóng sẽ đi qua trình biên dịch sau đó sẽ sử dụng thông tin này để xem lại biên dịch tối ưu hơn. & nbsp; 

Hãy nói rằng bạn có một chức năng, trả về một thuộc tính của một đối tượng:

function load(obj) {
return obj.x;

}

Trông đơn giản? Có thể với chúng tôi, nhưng đối với trình biên dịch, đây không phải là một nhiệm vụ đơn giản. Nếu trình biên dịch nhìn thấy một đối tượng mà nó không biết gì, thì nó phải kiểm tra thuộc tính x ở đâu, nếu đối tượng thực sự có thuộc tính như vậy, nó ở đâu trong bộ nhớ, nó có trong chuỗi nguyên mẫu và nhiều hơn nữa.

Vậy nó làm gì để tối ưu hóa nó?

Để hiểu rằng, chúng ta phải biết rằng trong mã máy, đối tượng được lưu với các loại của nó. & NBSP;

Hãy giả sử chúng ta có một đối tượng có thuộc tính X và Y, x có số loại và y là loại chuỗi. Về mặt lý thuyết, đối tượng sẽ được biểu diễn trong mã máy như thế này:

obj:

x: number

y: string

Tối ưu hóa có thể được thực hiện nếu chúng ta gọi một hàm có cùng cấu trúc đối tượng. Điều này có nghĩa là các thuộc tính sẽ giống nhau và theo cùng một thứ tự, nhưng các giá trị có thể khác nhau, như thế này:

load({x: 1, y: 'hello'});

load({x: 5, y: 'world'});

load({x: 3, y: 'foo'});

load({x: 9, y: 'bar'});

Ở đây, cách thức hoạt động của nó. Khi chúng tôi gọi hàm đó, trình biên dịch được tối ưu hóa sẽ nhận ra chúng tôi đang cố gắng gọi một hàm mà Lừa đã được gọi lại.

Sau đó, nó sẽ tiến hành kiểm tra xem đối tượng mà Lừa được truyền như một đối số có cùng thuộc tính hay không. & NBSP;

Nếu vậy, nó sẽ có thể truy cập vị trí của nó trong bộ nhớ, thay vì xem qua chuỗi nguyên mẫu và thực hiện nhiều việc khác được thực hiện cho các đối tượng không xác định.

Về cơ bản, trình biên dịch chạy qua một quá trình tối ưu hóa và tối ưu hóa. & NBSP;

Khi chúng tôi chạy mã, trình biên dịch giả định rằng một hàm sẽ sử dụng cùng các loại mà nó đã sử dụng trước đó, do đó, nó lưu mã trước với các loại trước. Loại mã này được gọi là mã máy được tối ưu hóa.

Mỗi khi mã gọi cùng một hàm, trình biên dịch được tối ưu hóa sau đó sẽ cố gắng truy cập cùng một vị trí trong bộ nhớ. & NBSP;

Nhưng bởi vì JS là một ngôn ngữ được gõ động, đến một lúc nào đó chúng ta có thể muốn sử dụng cùng một chức năng với các loại khác nhau. Trong trường hợp như vậy, trình biên dịch sẽ thực hiện một quá trình khử tối ưu hóa và biên dịch mã bình thường.

Để tóm tắt phần về trình biên dịch JIT, công việc của trình biên dịch JIT là cải thiện hiệu suất bằng cách sử dụng các phân đoạn mã nóng, khi trình biên dịch thực thi mã mà trước đó đã được thực thi, nó giả định rằng các loại giống nhau và sử dụng mã được tối ưu hóa mà đã được tạo ra. Nếu các loại khác nhau, JIT thực hiện tối ưu hóa và biên dịch mã bình thường.

Một lưu ý về hiệu suất

Một cách để cải thiện hiệu suất của ứng dụng của bạn là sử dụng cùng loại với các đối tượng khác nhau. Nếu bạn có hai đối tượng khác nhau có cùng loại, mặc dù các giá trị khác nhau miễn là các thuộc tính theo cùng một thứ tự và có cùng loại, thì trình biên dịch sẽ xem hai đối tượng này là một đối tượng có cấu trúc và loại bằng nhau và nó có thể truy cập nó nhanh hơn.

Ví dụ:

const obj = {
 x: 1,
 a: true,
 b: 'hey'

}

const obj2 = {
 x: 7,
 a: false,
 b: 'hello'

}

Như bạn có thể thấy trong ví dụ, chúng tôi có hai đối tượng khác nhau với các giá trị khác nhau, nhưng vì thứ tự và loại thuộc tính là như nhau, trình biên dịch sẽ có thể biên dịch các đối tượng này nhanh hơn.

Mặc dù nó có thể tối ưu hóa mã theo cách này, nhưng ý kiến ​​của tôi là có nhiều điều quan trọng hơn để làm cho hiệu suất, và một cái gì đó nhỏ như điều này không nên liên quan đến bạn.

Nó cũng khó có thể thực thi một cái gì đó như thế này trong một đội, và nhìn chung không có vẻ như tạo ra sự khác biệt lớn vì động cơ rất nhanh.

Với điều đó đã được nói, tôi đã thấy mẹo này được đề xuất bởi một thành viên trong nhóm V8, vì vậy đôi khi bạn muốn cố gắng theo dõi nó. Tôi thấy không có hại gì khi tuân theo nó khi có thể, nhưng chắc chắn không phải là chi phí của mã sạch và các quyết định kiến ​​trúc.

Bản tóm tắt

  1. Mã JS phải chạy trong một môi trường, những cái phổ biến nhất là trình duyệt và node.js.
  2. Môi trường cần phải có một động cơ, lấy mã JS được viết bằng cú pháp có thể đọc được của con người và biến nó thành mã máy.
  3. Độ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ó chính xác không. Nếu có bất kỳ lỗi nào, mã sẽ ngừng thực thi và lỗi sẽ được ném.
  4. 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 cây được gọi là cây cú pháp trừu tượng (AST).
  5. AST là một cấu trúc dữ liệu đại diện cho mã trong cấu trúc giống như cây. Nó dễ dàng hơn để biến mã thành mã máy từ AST.
  6. Trình thông dịch sau đó tiến hành lấy AST và biến nó thành IR, đây là sự trừu tượng của mã máy và 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.
  7. Trình biên dịch JIT sau đó lấy IR được tạo 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.

Bài đăng này ban đầu được xuất bản tại BorderlessEngineer.com