Cách thức hoạt động của Javascript Phần 1 : Tổng quan về engine, thời gian thực thi và call stack

MVT
Đang cập nhật

javascript là một trong những ngôn ngữ cực kỳ hot trong những năm gần đây, kể từ khi Google phát triển V8 engine vào năm 2008, rồi nối tiếp sau đó vài tháng, một mã nguồn mở, một môi trường back-end thực thi javascript chạy trên Chrome V8 engine và thực thi chương trình javascript ngoài trình duyệt web ra đời, đó là Node.js, đánh dấu thời điểm javascript bắt đầu trở thành ngôn ngữ lập trình được viết cho cả front-end lẫn back-end, hay nói ngắn gọn là full-stack. Trước đó, để lập trình web full-stack, một lập trình viên cần có rất nhiều kỹ năng, chỉ viết code không thôi vẫn là chưa đủ. Khi cần lưu thông tin, ta cần phải đưa chúng vào cơ sở dữ liệu (database). Sau khi hoàn thành code, phải tìm cách deploy nó, tức là đưa code lên một chỗ nào đó để nó thực thi chương trình. Một chương trình hoàn thiện không chỉ có code mà cần có nền tảng hệ điều hành và những phần mềm đi kèm với nó. Thời điểm đó LAMP Stack nổi lên như thế lực thống trị, được hầu hết website sử dụng. Stack này bào gồm : Linux, Apache, MySQL, PHP. Đến vài năm trở lại đây, có một vài stack nội lên như MEAN stack bao gồm : MongoDB, ExpressJS, Angular, Nodejs. Angular là một framework front-end được Google team phát triển vào năm 2010, được sử dụng rất thịnh hành từ sau V8 Engine được ra mắt. Đến năm 2013 ReactJS được phát triển bởi Facebook Team được sử dụng rộng rãi hơn và dẫn thay thế cho Angular, người ta thường gọi tắt là MERN stack cho dị thể này. Nếu bạn chưa đo lường được đọ "hot" của ngôn ngữ này, cùng tham khảo số liệu thống kê từ Github nhé.

Tính đến thời điểm quý 4 năm 2020, Javascript vẫn đang thống trị trên "bảng xếp hạng" số lượng người sử dụng.

Vậy, tại sao Javascript lại hot đến như vậy?

Tổng quan

Hầu như mọi developer khi sử viết code bằng javascript đều đã từng nghe đến khái niệm V8 Engine, và cũng biết rằng javascript là ngôn ngữ chạy đơn luồng (single-thread) sử dụng callstack queue, tức là trong một thời điểm chỉ xử lý 1 luồng thông tin.

Trong bài viết này, chúng ta sẽ đi qua chi tiết những khái niệm trên và giải thích cách mà Javascript thực thi chương trình bên trong nó. Nếu nắm được những khái niệm này, bạn có thể viết code tốt hơn, hiểu được các hàm trong javascrip chạy Non-blocking như thế nào, bạn có thể cải thiện kỹ thuật code cho mình.

Nếu bạn mới làm quen với javascript, bài viết này sẽ giúp bạn hiểu được sự khác biệt giữa javascript với những ngôn ngữ khác. Còn nếu bạn là một developer có nhiều kinh nghiệm, hy vọng nó sẽ đem lại cái nhìn khác về cách Javascript Runtime mà bạn đang làm việc với nó mỗi ngày.

JavaScript Engine

Một ví dụ điển hình về engine trong javascript đó là V8 Engine được phát triển bởi Google Team năm 2008. V8 Engine được sử dụng trong ChromeNode.js. Đơn giản, nó giống như thế này :

Engine này bao gồm 2 phần chính : - Memory Heap : Đây là nơi cấp vùng nhớ. - Call Stack : Đây là nơi để khi thực thi chương trình, code được đưa vào để xứ lý theo thứ tự

Runtime

Có một số Web APIs được sử dụng khá phổ biến như setTimeout, setInterval, AJAX, DOM. Tuy nhiên, chúng không được cung cấp bởi JS Engine. Vậy, chúng đến từ đâu?

JS Runtime là một bức tranh lớn và phức tạp hơn chứ không chỉ gói gọn trong JS Engine. Trong phạm vi bài viết chúng ta sẽ hiểu đầy đủ JS Runtime là browser's JS runtime environment. Và nó bao gồm những thành phần sau đây:

Chúng ta tạm gọi DOM, AJAX, setTimeout... được cung cấp bởi browser là Web APIs.

Còn event loopcallback queue là gì? Chúng đóng vai trò như thế nào trong JS Runtime?

The Call Stack

Như đã được nói từ những phần trên, Javascript là ngôn ngữ lập trình sử dụng đơn luồng (single thread), điều đó cũng có nghĩa là nó chỉ có một Call Stack chỉ thực hiện một công việc tại một thời điểm

Call Stack là một cấu trúc dữ liệu dạng ngăn xếp (stack) dùng để chứa thông tin về hoạt động của chương trình máy tính trong lúc thực thi.

Để rõ hơn, xét một ví dụ nhé :

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

Nếu bạn đã từng debug code kiểu nhảy từng dòng lệnh, thường thì các IDE sẽ cung cấp luôn một giao diện để chúng ta xem call stack hiện tại. Nôm na là khi bạn debug/step đến một function A, thì A sẽ được push (on top) vào call stack. Sau khi A thực thi xong và trả về kết quả, A sẽ bị pop ra khỏi stack.

Call Stack ban đầu sẽ trống trơn khi engine bắt đầu thực thi đoạn code. Ngay sau đó, từng step sẽ giống như trên. Mỗi step bạn thấy trong hình là một entry hay một bản ghi trong Call Stack và được gọi là Stack Frame.

Và đây cũng chính là cách mà stack traces được xây dựng để khi xảy ra các tình huống ngoại lệ, nó sẽ ném ra lỗi. VD:

function funcA(){
    console.log("This is function A")
}

function funcB(){
    throw new Error("This is error")
}

function getFuncs(){
    funcA();
    funcB();
}

getFuncs()

Nếu đoạn code này được thực thi trên trình duyệt Chrome, bạn sẽ thấy một thông báo lỗi xuất hiện trên màn hình console :

Blowing the stack hay gọi khác là Stack Overflow -có thể hiểu đó là hiện tượng tràn bộ nhớ, điều này xảy ra khi chương trình vượt qua kích thước tối đa của Call Stack, thường bị gây ra bởi sự lặp vô hạn của một function, chúng ta có thể dễ dàng bắt gặp khi sử dụng các hàm đệ quy (recursion) nếu như không test code kỹ càng.VD :

function foo(){
  foo()
}
foo()

Khi thực thi chương trình, với mã code trên, nó bắt đầu gọi hàm foo. Khi hàm foo được gọi bên trong hàm foo cha, nó lại tiếp tục gọi hàm foo con của nó, và cứ thế mã code trên cứ từ foo cha gọi đến foo con, foo con lại gọi foo con của nó, cứ như thế cho đến không bao giờ có sự kết thúc. Do đó, nó xảy ra hiện tường tràn bộ nhớ.

Trong trình duyệt Chrome, nếu hiện tượng overstack xảy ra nó sẽ báo lỗi như sau :

Thực thi code sử dụng đơn luồng có thể khá dễ dàng kiểm soát vì chúng ta không cần phải giải quyết các tình huống phức tạp điều mà xuất hiện phổ biến trong các ngôn ngữ lập trình sử dụng môi trường đa luồng - như là deadlocks.

Nhưng thực thi trong môi trường đơn luồng cũng có những điểm hạn chế. Vì JS chỉ có một Call Stack, vậy chuyện gì sẽ xảy ra khi mọi thứ trở nên chậm chạp?

Concurrency & the Event Loop

Chuyện gì xảy ra khi bạn có một function được gọi trong CallStack mà nó chiếm một lượng lớn thời gian để xử lý? Giả sử bạn muốn làm một vài sự chuyển đổi hình ảnh phức tạp trên trình duyệt với JS. Do trong quá trình chuyển đổi, máy tính sẽ phải xử lý rất nhiều vấn để phức tạp, do đó thời gian thực thi khá lâu, trong thời gian thực thi đó, trình duyệt chỉ tập trung xử lý 1 công việc hiện tại, thực sự không thể làm được điều gì khác, nó bị block. Điều này đồng nghĩa trình duyệt không thể render, không thể chạy bất ký đoạn code nào khác, nó bị kẹt cứng. Và đó cũng là vấn đề bạn cần quan tâm khi để cập đến một ứng dụng có UI/UX tốt.

Ngoài việc xử lý một dữ liệu với thời gian lớn, bạn cũng có thể gặp một tình huống khác cũng oái ăm không kém, đó là xử lý quá nhiều công việc đòi hỏi lượng lớn thời gian trong một CallStack. Khi thời gian chờ để hoàn tất mọi công việc trong một CallStack quá lâu, nó sẽ gửi phản hồi đến bạn, hỏi bạn liệu có muốn loại bỏ trang web đang xử lý hay không.

Điều này chắc hẳn không phải là điều mà người dùng mong muốn. Vậy làm như thế nào để chúng ta có thể thực thi những đoạn mã code nặng mà không bị blocking UI và không bị trình duyệt đưa ra phản hồi không mong muốn như thế này? Giải pháp cho bạn là sử dụng asynchronous callbacks.

Khái niệm này sẽ được giải thích trong phần 2 trong chuyên để "Cách thức hoạt động của Javascript , tổng quan về engine, thời gian thực thi và call stack" : "Cốt lõi của V8 engine và 5 mẹo để viết code được tối ưu". Mới các bạn đón đọc.


Bài viết có liên quan