Hiểu về JavaScript không đồng bộ – Các event loop
Biên dịch: Nguyễn Văn Lợi Tìm hiểu cách hoạt động của JavaScript JavaScript là một ngôn ngữ lập trình đơn luồng có nghĩa là chỉ có một điều có thể xảy ra tại một thời điểm. Tức là, công cụ JavaScript chỉ có thể xử lý một câu lệnh tại một thời điểm trong một chuỗi duy nhất. ...
Biên dịch: Nguyễn Văn Lợi
Tìm hiểu cách hoạt động của JavaScript
JavaScript là một ngôn ngữ lập trình đơn luồng có nghĩa là chỉ có một điều có thể xảy ra tại một thời điểm. Tức là, công cụ JavaScript chỉ có thể xử lý một câu lệnh tại một thời điểm trong một chuỗi duy nhất.
Mặc dù ngôn ngữ đơn luồng đơn giản hóa mã viết vì bạn không phải lo lắng về các vấn đề đồng thời, điều này cũng có nghĩa là bạn không thể thực hiện các hoạt động dài như truy cập mạng mà không chặn luồng chính.
Hãy tưởng tượng yêu cầu một số dữ liệu từ một API. Tùy thuộc vào tình hình máy chủ có thể mất một thời gian để xử lý yêu cầu trong khi chặn chủ đề chính làm cho trang web không hồi đáp.
Đó là nơi JavaScript không đồng bộ được phát. Sử dụng JavaScript không đồng bộ (chẳng hạn như callbacks, promise và async / await), bạn có thể thực hiện các yêu cầu mạng dài mà không chặn luồng chính.
Mặc dù không cần thiết bạn phải học tất cả các khái niệm này để trở thành một nhà phát triển JavaScript tuyệt vời, nhưng thật hữu ích khi biết cách hoạt động của nó.
Vì vậy, không cần nói thêm, hãy bắt đầu.
JavaScript đồng bộ hoạt động như thế nào?
Trước khi chúng ta đi sâu vào JavaScript không đồng bộ, trước tiên hãy hiểu cách mã JavaScript đồng bộ thực thi bên trong công cụ JavaScript. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 |
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); |
Để hiểu cách mã trên thực thi bên trong công cụ JavaScript, chúng ta phải hiểu khái niệm ngữ cảnh thực thi và gọi ngăn xếp cuộc gọi (còn được gọi là ngăn xếp thực hiện).
Ngữ cảnh thực thi
Một bối cảnh thực thi là một khái niệm trừu tượng về môi trường nơi mã JavaScript được đánh giá và thực hiện. Bất cứ khi nào bất kỳ mã nào được chạy trong JavaScript, nó chạy bên trong một ngữ cảnh thực thi.
Mã hàm thực thi bên trong ngữ cảnh thực thi hàm và mã toàn cục thực hiện bên trong ngữ cảnh thực thi chung. Mỗi hàm có ngữ cảnh thực thi riêng của nó.
Ngăn xếp cuộc gọi
Ngăn xếp cuộc gọi như tên gọi của nó ngụ ý là một ngăn xếp với cấu trúc LIFO (Last in, First out), được sử dụng để lưu trữ tất cả ngữ cảnh thực thi được tạo ra trong quá trình thực thi mã.
JavaScript có một ngăn xếp cuộc gọi duy nhất vì nó là một ngôn ngữ lập trình đơn luồng. Ngăn xếp cuộc gọi có cấu trúc LIFO có nghĩa là các mục có thể được thêm hoặc xóa khỏi đầu ngăn xếp chỉ.
Hãy quay lại đoạn mã trên và cố gắng hiểu cách mã thực thi bên trong công cụ JavaScript.
1 2 3 4 5 6 7 8 9 10 11 |
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); |
Vậy điều gì đang xảy ra ở đây?
Khi mã này được thực thi, một bối cảnh thực thi toàn cục được tạo ra (được biểu diễn bằng hàm main() ) và được đẩy lên trên cùng của ngăn xếp cuộc gọi. Khi cuộc gọi đến first() gặp phải, nó được đẩy lên trên cùng của ngăn xếp.
Tiếp theo, console.log('Hi there!') Được đẩy lên trên cùng của ngăn xếp, khi nó kết thúc, nó xuất hiện từ ngăn xếp. Sau đó, chúng ta gọi hàm second() , do đó hàm second() được đẩy lên trên cùng của ngăn xếp.
console.log('Hello there!') được đẩy lên trên cùng của ngăn xếp và bật ra khỏi ngăn xếp khi nó kết thúc. Hàm second() kết thúc, do đó, nó được bật ra khỏi ngăn xếp.
console.log('The End') được đẩy lên trên cùng của ngăn xếp và loại bỏ khi nó kết thúc. Sau đó, hàm first() hoàn thành, do đó nó được loại bỏ khỏi ngăn xếp.
Chương trình hoàn thành thực hiện tại thời điểm này, do đó, bối cảnh thực thi toàn cục ( main() ) được bật ra khỏi ngăn xếp.
JavaScript không đồng bộ hoạt động như thế nào?
Bây giờ chúng ta có một ý tưởng cơ bản về ngăn xếp cuộc gọi và cách JavaScript hoạt động đồng bộ, hãy quay lại JavaScript không đồng bộ.
Chặn là gì?
Giả sử chúng ta đang xử lý hình ảnh hoặc yêu cầu mạng theo cách đồng bộ. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const processImage = (image) => { /** * doing some operations on image **/ console.log('Image processed'); } const networkRequest = (url) => { /** * requesting network resource **/ return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting(); |
Việc xử lý hình ảnh và yêu cầu mạng cần có thời gian. Vì vậy, khi hàm processImage() được gọi, nó sẽ mất một thời gian tùy thuộc vào kích thước của hình ảnh.
Khi hàm processImage() hoàn thành, nó được lấy ra khỏi ngăn xếp. Sau đó hàm networkRequest() được gọi và được đẩy vào ngăn xếp. Một lần nữa nó cũng sẽ mất một thời gian để hoàn thành thực hiện.
Cuối cùng khi hàm networkRequest() hoàn thành, hàm greeting() được gọi và vì nó chỉ chứa một câu lệnh console.log và các câu lệnh console.log nói chung là nhanh, do đó hàm greeting() được thực hiện ngay lập tức và trả về.
Vì vậy, bạn thấy, chúng ta phải đợi cho đến khi hàm (như processImage() hoặc networkRequest() ) kết thúc. Điều này có nghĩa là các chức năng này đang chặn ngăn xếp cuộc gọi hoặc chuỗi chính. Vì vậy, chúng tôi không thể thực hiện bất kỳ thao tác nào khác trong khi mã trên thực thi không phải là lý tưởng.
Vậy giải pháp là gì?
Giải pháp đơn giản nhất là gọi lại không đồng bộ. Chúng tôi sử dụng gọi lại không đồng bộ để làm cho mã của chúng tôi không bị chặn. Ví dụ:
1 2 3 4 5 6 7 8 9 |
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); |
Ở đây tôi đã sử dụng phương thức setTimeout để mô phỏng yêu cầu mạng. Xin lưu ý rằng setTimeout không phải là một phần của công cụ JavaScript, nó là một phần của một cái gì đó được gọi là API web (trong trình duyệt) và API C / C ++ (trong node.js).
Để hiểu cách mã này được thực thi, chúng ta phải hiểu thêm một vài khái niệm vòng lặp sự kiện như vậy và hàng đợi gọi lại (hoặc hàng đợi thông báo).
Vòng lặp sự kiện, API web và hàng đợi thông báo không phải là một phần của công cụ JavaScript, nó là một phần của môi trường chạy JavaScript của trình duyệt hoặc môi trường chạy JavaScript Nodejs (trong trường hợp Nodejs). Trong Nodejs, các API web được thay thế bằng các API C / C ++.
Bây giờ chúng ta hãy quay trở lại đoạn code trên và xem nó được thực hiện như thế nào theo một cách không đồng bộ.
1 2 3 4 5 6 7 8 9 10 |
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End'); |
Khi mã trên tải trong trình duyệt, console.log('Hello World') được đẩy vào ngăn xếp và bật ra khỏi ngăn xếp sau khi nó kết thúc. Tiếp theo, một cuộc gọi đến networkRequest() là bắt gặp, vì vậy nó được đẩy lên trên cùng của ngăn xếp.
Hàm setTimeout() tiếp theo được gọi, do đó nó được đẩy lên trên cùng của ngăn xếp. Hàm setTimeout() có hai đối số: 1) gọi lại và 2) thời gian tính bằng mili giây (mili giây).
Phương thức setTimeout() bắt đầu hẹn giờ 2s trong môi trường API web. Tại thời điểm này, setTimeout() đã kết thúc và nó được bật ra khỏi ngăn xếp. Sau đó, console.log('The End') được đẩy vào ngăn xếp, được thi hành và bị xóa khỏi ngăn xếp sau khi hoàn thành.
Trong khi đó, bộ hẹn giờ đã hết hạn, bây giờ gọi lại được đẩy đến hàng đợi tin nhắn . Nhưng cuộc gọi lại không được thực hiện ngay lập tức, và đó là nơi mà vòng lặp sự kiện khởi động.
Vòng lặp sự kiện
Công việc của vòng lặp Sự kiện là nhìn vào ngăn xếp cuộc gọi và xác định xem ngăn xếp cuộc gọi có trống hay không. Nếu ngăn xếp cuộc gọi trống, nó sẽ nhìn vào hàng đợi tin nhắn để xem có bất kỳ cuộc gọi chờ đang chờ xử lý nào đang chờ được thực thi hay không.
Trong trường hợp này, hàng đợi tin nhắn chứa một cuộc gọi lại và ngăn xếp cuộc gọi trống tại thời điểm này. Vì vậy, vòng lặp Sự kiện đẩy cuộc gọi lại đến đầu ngăn xếp.
Sau đó, console.log('Async Code') được đẩy lên trên cùng của ngăn xếp, được thực hiện và xuất hiện từ ngăn xếp. Tại thời điểm này, cuộc gọi lại đã kết thúc để nó bị xóa khỏi ngăn xếp và chương trình cuối cùng cũng kết thúc.
DOM Event
Hàng đợi Tin nhắn cũng chứa các cuộc gọi lại từ các sự kiện DOM như sự kiện nhấp chuột và các sự kiện bàn phím. Ví dụ:
1 2 3 4 5 |
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); }); |
Trong trường hợp các sự kiện DOM, trình lắng nghe sự kiện nằm trong môi trường API web đang chờ một sự kiện nào đó (sự kiện nhấp chuột trong trường hợp này) xảy ra, và khi sự kiện đó xảy ra, thì hàm gọi lại được đặt trong hàng đợi tin nhắn đang đợi để được thực thi .
Một lần nữa vòng lặp sự kiện sẽ kiểm tra xem ngăn xếp cuộc gọi có trống không và đẩy sự kiện gọi lại đến ngăn xếp nếu nó trống và gọi lại được thực thi.
ES6 Job Queue/ Micro-Task queue
Chúng ta đã học được cách các cuộc gọi lại không đồng bộ và các sự kiện DOM được thực thi, sử dụng hàng đợi thông điệp để lưu trữ tất cả các cuộc gọi lại chờ đợi để được thực thi.
ES6 giới thiệu khái niệm về hàng đợi công việc được sử dụng bởi Promises trong JavaScript. Sự khác biệt giữa hàng đợi tin nhắn và hàng đợi công việc là hàng đợi công việc có mức độ ưu tiên cao hơn hàng đợi thông báo, nghĩa là các công việc hứa hẹn bên trong hàng đợi công việc sẽ được thực hiện trước khi gọi lại bên trong hàng đợi tin nhắn.
1 2 3 4 5 6 7 8 9 10 11 |
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End'); |
Output:
1 2 3 4 5 6 |
Script start Script End Promise resolved setTimeout |
Chúng ta có thể thấy rằng lời hứa được thực thi trước setTimeout , bởi vì đáp ứng lời hứa được lưu trữ bên trong hàng đợi công việc có mức độ ưu tiên cao hơn hàng đợi thông báo.
Phần kết luận
Vì vậy, chúng ta đã học được cách JavaScript không đồng bộ hoạt động và các khái niệm khác như ngăn xếp cuộc gọi, vòng lặp sự kiện, hàng đợi tin nhắn và hàng đợi công việc cùng nhau tạo môi trường chạy JavaScript. Mặc dù bạn không cần phải học tất cả các khái niệm này để trở thành một nhà phát triển JavaScript tuyệt vời nhưng sẽ rất hữu ích khi biết các khái niệm này.
TopDev via Medium