Cách thức hoạt động của Javascript: Tổng quan về engine, runtime & call stack

2297

Các Tech teams hiện nay đang khai thác tối đa mọi sự hỗ trợ của Javascript ở nhiều cấp độ trong stack của team như Frontend, Backend, Hybrid apps, các thiết bị nhúng…

Bài viết này sẽ mở đầu cho series nghiên cứu sâu về Javascript, cách thức hoạt động của Javascript, tích lũy từ quá trình nắm rõ các building blocks của Javascript và cách kết hợp chúng để viết code & apps tốt hơn.

Như đã thấy trong GitHut stats, JavaScript nằm top đầu trong Active Repositories và Total Pushes trên GitHub.

(Xem thứ hạng ngôn ngữ lập trình cập nhật trên GitHub).

Nếu các dự án đang ngày càng trở nên độc lập hơn với JavaScript, đồng nghĩa là developer phải tận dụng mọi thứ mà Javascript & hệ sinh thái cung cấp với sự thấu hiểu ngày càng sâu hơn về những yếu tố nội bộ nhằm xây dựng nên 1 phần mềm tốt hơn.

Có rất nhiều developer đang sử dụng Javascript mỗi ngày nhưng lại không nắm được kiến thức cơ bản.

Tổng quan

Hầu hết mọi người đều đã nghe đến V8 Engine và hầu hết mọi người đều biết Javascript là single-thread hoặc biết rằng Javascript đang sử dụng 1 callback queue.

Trong post này, chúng ta sẽ đào sâu các concepts này và giải thích rõ cách hoạt động của Javascript. Nắm được chi tiết những điều này, bạn sẽ có thể viết được các ứng dụng tốt hơn, không bị block và khai thác các APIs đã cung cấp 1 cách phù hợp.

Nếu bạn mới tiếp xúc với Javascript, bài viết này sẽ giúp bạn hiểu lý do tại sao Javascript khá “kì quặc” so với các ngôn ngữ khác.

Và nếu bạn là 1 developer Javascript kinh nghiệm thì hy vọng tôi có thể đem đến vài insights tươi mới liên quan đến Javascript Runtime mà bạn đang sử dụng hằng ngày.

JavaScript Engine

Ví dụ phổ biến của JavaScript Engine là V8 engine của Google. Ví dụ, V8 engine được sử dụng trong Chrome và Node.js. Đây là tổng quan đơn giản về Javascript Engine.

Engine sẽ gồm 2 components chính:

  • Memory Heap — nơi diễn ra memory allocation
  • Call Stack — nơi các stack frames tồn tại khi code của bạn thực thi

Runtime

Có các APIs trong hệ điều hành được hầu hết các JavaScript developer sử dụng ngoài kia (như “setTimeout”). Tuy nhiên, các API đó không do Engine cung cấp.

Vậy chúng đến từ đâu?

Thực tế sẽ phức tạp hơn.

Như vậy, chúng ta đã có Engine nhưng thực sự có nhiều thứ khác nữa, như Web APIs được cung cấp bởi các hệ điều hành như DOM, AJAX, setTimeout…

Và sau đó, chúng ta có event loop & callback queue rất phổ biến.

Call Stack

JavaScript là ngôn ngữ lập trình single-threaded, đồng nghĩa là nó có Call Stack single.

Call Stack là 1 data structure ghi lại nơi chúng ta hiện diện trong program. Nếu nói về 1 function, chúng ta đặt nó lên đầu stack. Nếu chúng ta trả về từ 1 function, thì lấy nó ra khỏi phần đầu. Đó chính là tất cả những gì mà stack có thể làm.

Cùng xem ví dụ sau.

Khi engine bắt đầu thực hiện đoạn code này, Call Stack sẽ trống. Sau đó, các bước tiếp theo sẽ như sau:

Mỗi entry trong Call Stack được gọi là 1 Stack Frame.

Và đây chính xác là cách xây dựng các stack traces khi 1 exception đang được “ném” – về cơ bản nó là state của Call Stack khi exception xảy ra. Xem đoạn code sau:

Nếu đoạn code được thực thi trong Chrome (giả định code này nằm trong file tên là foo.js), stack trace theo sau sẽ được xây dựng như sau:

Blowing the stack”  xảy ra khi bạn đến được kích cỡ Call Stack tối đa. Và việc này diễn ra khá dễ dàng đặc biệt là trong trường hợp đang sử dụng giải thuật đệ quy mà không test code quá rộng rãi. Cùng xem đoạn code mẫu:

Khi engine bắt đầu thực thi đoạn code này, engine sẽ bắt đầu bằng việc gọi function “foo”. Tuy nhiên, function này là đệ quy và bắt đầu tự call mà không có bất kì điều kiện chấm dứt nào. Vì vậy, ở mỗi bước execution, function tương tự sẽ được thêm vào Call Stack hết lần này đến lần khác.

Tuy nhiên, trong vài thời điểm, số lượng function calls trong Call Stack sẽ vượt quá kích thước thực sự của Call Stack và hệ điều hành sẽ quyết định hành động bằng cách ném 1 error trông như này:

Chạy code trong single thread khá dễ vì bạn không phải xử lý các tình huống phức tạp phát sinh trong những môi trường multi-threaded —chẳng hạn như deadlocks.

Tuy nhiên, chạy trong 1 single thread cũng có giới hạn của nó. Vì JavaScript có 1 single Call Stack, nên chuyện gì sẽ xảy ra khi mọi thứ bị chậm?

Concurrency & Event Loop

Chuyện gì xảy ra khi bạn có function calls trong Call Stack ngốn rất nhiều thời gian để xử lý? Ví dụ, thử tưởng tượng bạn muốn thực hiện vài chuyển đổi hình ảnh phức tạp trong hệ điều hành.

Vấn đề chính là khi Call Stack có các functions để thực thi, hệ điều hành không thể thực sự làm điều gì khác vì bị block. Đồng nghĩa là hệ điều hành không thể render, không thể chạy code nào khác và bị mắc kẹt. Việc này sẽ gây ra vấn đề nếu bạn muốn có fluid UIs tốt trong ứng dụng của mình.

Thêm nữa, một khi hệ điều hành bắt đầu xử lý quá nhiều tasks trong Call Stack, nó sẽ ngừng responsive trong 1 quãng thời gian khá dài. Và hầu hết các browsers sẽ hành động bằng cách đưa ra error, hỏi bạn là liệu có muốn ngừng web page này không.

Trải nghiệm người dùng không tốt chút nào, đúng không?

Vậy làm cách nào để có thể thực thi các đoạn code này mà không chặn UI & khiến cho browser không responsive nữa? Giải pháp ở đây là asynchronous callbacks giải pháp sẽ được giải thích chi tiết hơn trong Phần 2 của tutorial “How JavaScript actually works” là “Inside the V8 engine + 5 tips on how to write optimized code”.

Trong khi đó, nếu bạn đang gặp khó khăn trong việc reproduce và hiểu rõ các vấn đề diễn ra trong các ứng dụng Javascript của mình thì hãy xem qua SessionStack. SessionStack ghi lại mọi thứ trong web apps của bạn: những thay đổi của DOM, user interactions, các exceptions của JavaScript, stack traces, các network requests bị lỗi và các tin nhắn debug.

Với SessionStack, bạn có thể replay các vấn đề trong web apps như videos và quan sát mọi thứ diễn ra với user.

Bắt đầu hoàn toàn miễn phí ở đây.

Nguồn: TopDev via blog.sessionstack.com