Cơ chế hoạt động của javascript và nodejs
Cuộc đời là đóa hoa mà tình yêu là mật ngọt. -- Victor Hugo Các bạn có thể đọc bài viết gốc tại đây Trong javascript, hầu hết các lời gọi I/O đều là non-blocking. Nghĩa là khi có HTTP request, truy xuất dữ liệu trong DB hoặc đọc ghi vào bộ nhớ thì hệ thống sẽ không tạm dừng ...
Cuộc đời là đóa hoa mà tình yêu là mật ngọt.
-- Victor Hugo
Các bạn có thể đọc bài viết gốc tại đây
Trong javascript, hầu hết các lời gọi I/O đều là non-blocking. Nghĩa là khi có HTTP request, truy xuất dữ liệu trong DB hoặc đọc ghi vào bộ nhớ thì hệ thống sẽ không tạm dừng (blocking) các đoạn code tiếp theo (như các ngôn ngữ server khác PHP, Ryby,...) mà sẽ trao quyền thực thi những lời gọi I/O này cho hệ thống và thực thi những đoạn code tiếp theo, khi hệ thống đã thực thi xong những lời gọi hệ thống này thì hàm callback truyền vào sẽ tự động được gọi.
Ví dụ về blocking trong Ruby:
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
Trình tự hệ thống thực hiện đoạn code trên là:
Request tới http://www.google.com, lúc này hệ thống sẽ tạm dừng (blocking) để chờ kết qủa của request này trả về.
Thông tin trả về và được gán vào biến response.
Hiển thị response được trả về lên màn hình.
Hiển thị 'Done' lên màn hình.
Thứ tự thực hiển của các ngôn ngữ server thông thường (như PHP, Ruby) sẽ thực hiện kiểu synchronous (đồng bộ) từ trên xuống dưới như flow trên. Còn dưới đây là ví dụ về trình tự thực thi trong nodejs(javascript):
request('http://www.google.com', function(error, response, body) { console.log(body); }); console.log('Done!');
Lưu ý: Đoạn code trên cũng sẽ được gọi theo thứ tự từ trên xuồng dưới.
Câu lệnh đầu tiên sẽ gọi request tới http://www.google.com, không như trên, ở đây hệ thống sẽ không đợi kết qủa trả về mới thực thi tiếp. Hệ thống sẽ truyền hàm callback ở trên vào event loop để khi hàm request có kết qủa trả về thì sẽ thực thi hàm callback này ngay. Sau khi truyền hàm callback vào event loop thì hệ thống sẽ tiếp tục thực hiện câu lệnh phía dưới.
In ra màn hình 'Done!'
Vào thời điểm nào đó sau đó, khi request có kết qủa trả về thì hàm callback sẽ được gọi và in ra body của response.
Code như trên gọi là assynchronous (bất đồng bộ), nghĩa là hệ thống có thể thực hiện các đoạn code của mình một cách đồng thời(như vd trên Done! được in ra trước body trả về).
Nodejs giúp code của bạn chạy những đoạn còn lại trong khi vẫn chờ response trả về để thực thi hàm callback truyền vào. Thế nhưng câu hỏi đặt ra là hàm callback này được lưu trữ ở đâu, chúng được thực thi theo thứ tự nào khi có nhiều hàm callback như thế, và cái gì gọi chúng thực thi.
Trình khởi chạy của javascript có một hàng đợi (queue) chứa các messages, các messages này được gắn liền với các hàm callback truyền vào. Mỗi khi gặp câu lệnh có callback truyền vào thì message gắn với callback đó sẽ được đẩy vào queue, và khi các event được trigger( ví dụ như event click, event request có response trả về) thì hệ thống sẽ gọi hàm callback tương ứng để thực thi. Khi một event ví dụ như click vào button nhưng không truyền vào callback thì sẽ không có message nào được đẩy vào queue, nghĩa là chỉ những event có callback tryền vào thì mới được đẩy vào queue.
Hình trên mô tả hoạt động của Event Loop. Khi có trigger(sự kiện click hoặc request ở vd trên thực hiện xong và có response trả về) thì hàm callback truyền vào sẽ được đẩy vào Massage Queue.
Khi biên dịch đoạn code ban đầu thì các dòng lệnh assyn của bạn sẽ được đẩy vào stack, khi các dòng lệnh này chạy từ đầu tới cuối (chúng chạy song song với các tiến trình truy cập I/O nói ở trên) và khi chạy xong câu lệnh cuối cùng rồi thì stack sẽ về rỗng. Một khi stack về rỗng (chạy xong thân code của bạn), vòng lặp Event Loop sẽ được khởi chạy. Mỗi khi Event Loop gặp một message trong Message Queue, nó sẽ thực thi hàm callback gắn với message đó bằng cách đẩy các đoạn code trong hàm callback vào stack. Sau khi hàm callback đó thực hiện xong, stack về rỗng, thì Event Loop tiếp tục chạy và lấy message tiếp theo (nếu có) ra và đẩy code của callback vào stack thực thi.
Như giải thích ở phần 2, nodejs là single thread non-blocking (hiểu thêm về thread ở bài giới thiệu thread từ phần cứng ra phần mềm) nên các request tới sau sẽ được thực hiện ngay sau khi thân code của request thứ nhất được thực hiện xong, thân code ở đây là mọi thứ đang có trong call stack và message queue, những callback nằm trong message queue là những callback đã có kết quả trả về, còn những callback thuộc về request thứ nhất mà chưa có kết quả trả về sẽ chưa được đẩy vào message queue.
Câu hỏi đặt ra là request thứ 2 tới sẽ được đặt ở đâu để chờ cho request thứ nhất thực hiện xong, request cũng là một event nên nó sẽ được đặt vào message queue(mình đoán vậy thôi vì search gg không thấy :)))), khi các callback được đẩy vào message queue trước request thứ 2 này được thực hiện xong thì request thứ 2 này mới được lấy ra vào cho vào call stack để thực hiện. Ở điểm này có thắc mắc là nếu request thứ 2 thay đổi biến global của chương trình và request thứ nhất vẫn còn callback(gọi là callback A) được đẩy vào message queue sau request thứ 2 và callback A này lại cần dữ liệu của biến global đó thì chẳng phải là callback A đã truy cập sai dữ liệu dẫn tới request thứ nhất trả về kết quả sai sao?
Các lời gọi I/O là assync (nhiều I/O được thực hiện cùng 1 lúc).
Các lời gọi hàm của bạn là sync (Code của bạn chạy tuần tự từ trên xuống dưới).
Các hàm callback sẽ chạy sau khi thân code của bạn thực thi xong.
Các hàm callback sẽ chạy tuần tự theo thứ tự callback nào được trigger trước sẽ chạy trước chứ không phải các hàm callback được chạy đồng thời.
=> Đây chính là single thread trong nodejs. Nghĩa là chỉ có một tiến trình được chạy trong code của bạn (Còn những lời gọi I/O assyn là của hệ thống gọi multi thread).