Nếu bạn đã học JavaScript được một thời gian, thì có lẽ bạn đã nghe thấy thuật ngữ “không đồng bộ” trước đây.
Điều này là do JavaScript là một ngôn ngữ không đồng bộ… nhưng điều đó thực sự có nghĩa là gì? Trong bài viết này, tôi hy vọng sẽ cho bạn thấy rằng khái niệm này không khó như bạn tưởng.
Trước khi bắt đầu tìm hiểu vấn đề thực sự, chúng ta hãy xem xét hai từ này – đồng bộ và không đồng bộ.
Theo mặc định, JavaScript là ngôn ngữ lập trình đơn luồng, đồng bộ. Điều này có nghĩa là các lệnh chỉ có thể chạy lần lượt và không chạy song song. Hãy xem xét đoạn mã nhỏ dưới đây:
let a = 1;
let b = 2;
let sum = a + b;
console.log(sum);
Đoạn mã trên khá đơn giản – nó tính tổng hai số và sau đó ghi tổng vào bảng điều khiển trình duyệt. Trình thông dịch thực hiện lần lượt các lệnh này theo thứ tự đó cho đến khi thực hiện xong.
Nhưng phương pháp này đi kèm với nhược điểm. Giả sử chúng tôi muốn tìm nạp một số lượng lớn dữ liệu từ cơ sở dữ liệu và sau đó hiển thị nó trên giao diện của chúng tôi. Khi trình thông dịch đạt được hướng dẫn tìm nạp dữ liệu này, phần còn lại của mã sẽ bị chặn thực thi cho đến khi dữ liệu được tìm nạp và trả về.
Bây giờ bạn có thể nói rằng dữ liệu được tìm nạp không quá lớn và sẽ không mất bất kỳ thời gian đáng chú ý nào. Hãy tưởng tượng rằng bạn phải tìm nạp dữ liệu ở nhiều điểm khác nhau. Sự chậm trễ này gộp lại không giống như điều gì đó mà người dùng muốn gặp phải.
Thật may mắn cho chúng tôi, các vấn đề với JavaScript đồng bộ đã được giải quyết bằng cách giới thiệu JavaScript không đồng bộ.
Hãy coi mã không đồng bộ là mã có thể bắt đầu ngay bây giờ và kết thúc quá trình thực thi sau. Khi JavaScript đang chạy không đồng bộ, các hướng dẫn không nhất thiết phải được thực hiện lần lượt như chúng ta đã thấy trước đây.
Để thực hiện đúng hành vi không đồng bộ này, có một vài giải pháp khác nhau mà các nhà phát triển đã sử dụng trong nhiều năm. Mỗi giải pháp đều cải thiện so với giải pháp trước đó, giúp mã được tối ưu hóa hơn và dễ hiểu hơn trong trường hợp mã trở nên phức tạp.
Để hiểu rõ hơn về bản chất không đồng bộ của JavaScript, chúng ta sẽ xem qua các chức năng gọi lại, lời hứa và không đồng bộ và chờ đợi.
Gọi lại là một chức năng được truyền bên trong một chức năng khác, sau đó được gọi trong chức năng đó để thực hiện một tác vụ.
Gây nhầm lẫn? Hãy phá vỡ nó bằng cách thực hiện nó một cách thực tế.
console.log('fired first');
console.log('fired second');
setTimeout(()=>{
console.log('fired third');
},2000);
console.log('fired last');
Đoạn mã trên là một chương trình nhỏ ghi nội dung vào bảng điều khiển. Nhưng có một cái gì đó mới ở đây. Trình thông dịch sẽ thực hiện lệnh đầu tiên, sau đó là lệnh thứ hai, nhưng nó sẽ bỏ qua lệnh thứ ba và thực hiện lệnh cuối cùng.
Các setTimeout
là một hàm JavaScript có hai tham số. Tham số đầu tiên là một hàm khác và tham số thứ hai là thời gian sau đó hàm đó sẽ được thực thi tính bằng mili giây. Bây giờ bạn đã thấy định nghĩa về các cuộc gọi lại đang phát huy tác dụng.
chức năng bên trong setTimeout
trong trường hợp này được yêu cầu chạy sau hai giây (2000 mili giây). Hãy tưởng tượng nó được mang đi thực thi trong một phần riêng biệt nào đó của trình duyệt, trong khi các hướng dẫn khác tiếp tục thực thi. Sau hai giây, kết quả của hàm sẽ được trả về.
Đó là lý do tại sao nếu chúng tôi chạy đoạn mã trên trong chương trình của mình, chúng tôi sẽ nhận được điều này:
fired first
fired second
fired last
fired third
Bạn thấy rằng hướng dẫn cuối cùng được ghi lại trước chức năng trong setTimeout
trả về kết quả của nó. Giả sử chúng tôi đã sử dụng phương pháp này để lấy dữ liệu từ cơ sở dữ liệu. Trong khi người dùng đang chờ lệnh gọi cơ sở dữ liệu trả về kết quả, luồng thực thi sẽ không bị gián đoạn.
Phương pháp này rất hiệu quả, nhưng chỉ ở một mức độ nhất định. Đôi khi, các nhà phát triển phải thực hiện nhiều cuộc gọi đến các nguồn khác nhau trong mã của họ. Để thực hiện các lệnh gọi này, các lệnh gọi lại được lồng vào nhau cho đến khi chúng trở nên rất khó đọc hoặc khó duy trì. Điều này được gọi là địa ngục gọi lại
Để khắc phục vấn đề này, lời hứa đã được giới thiệu.
Chúng ta luôn nghe mọi người hứa hẹn. Người anh họ của bạn, người đã hứa sẽ gửi tiền miễn phí cho bạn, một đứa trẻ hứa sẽ không chạm vào lọ bánh quy nữa khi chưa được phép… nhưng lời hứa trong JavaScript hơi khác một chút.
Một lời hứa, trong bối cảnh của chúng ta, là điều gì đó sẽ mất một thời gian để thực hiện. Có hai kết quả có thể xảy ra của một lời hứa:
- Chúng tôi hoặc chạy và giải quyết lời hứa, hoặc
- Một số lỗi xảy ra dọc theo dòng và lời hứa bị từ chối
Các lời hứa đã xuất hiện để giải quyết các vấn đề về chức năng gọi lại. Một lời hứa có hai chức năng làm tham số. Đó là, resolve
và reject
. Hãy nhớ rằng giải quyết là thành công và từ chối là khi xảy ra lỗi.
Hãy xem những lời hứa trong công việc:
const getData = (dataEndpoint) => {
return new Promise ((resolve, reject) => {
//some request to the endpoint;
if(request is successful){
//do something;
resolve();
}
else if(there is an error){
reject();
}
});
};
Đoạn mã trên là một lời hứa, kèm theo một yêu cầu tới một số điểm cuối. Lời hứa có trong resolve
và reject
như tôi đã đề cập trước đây.
Ví dụ: sau khi thực hiện cuộc gọi đến điểm cuối, nếu yêu cầu thành công, chúng tôi sẽ giải quyết lời hứa và tiếp tục làm bất cứ điều gì chúng tôi muốn với phản hồi. Nhưng nếu có lỗi, lời hứa sẽ bị từ chối.
Lời hứa là một cách gọn gàng để khắc phục các sự cố do địa ngục gọi lại gây ra, theo một phương pháp được gọi là xâu chuỗi lời hứa. Bạn có thể sử dụng phương pháp này để lấy dữ liệu theo trình tự từ nhiều điểm cuối, nhưng với ít mã hơn và phương pháp dễ dàng hơn.
Nhưng có một cách thậm chí còn tốt hơn! Bạn có thể quen thuộc với phương pháp sau, vì đây là cách ưa thích để xử lý dữ liệu và lệnh gọi API trong JavaScript.
Vấn đề là, xâu chuỗi các lời hứa với nhau giống như các cuộc gọi lại có thể trở nên khá cồng kềnh và khó hiểu. Đó là lý do Async và Await ra đời.
Để xác định chức năng không đồng bộ, bạn làm điều này:
const asyncFunc = async() => {
}
Lưu ý rằng việc gọi một chức năng không đồng bộ sẽ luôn trả về một Lời hứa. Hãy xem này:
const test = asyncFunc();
console.log(test);
Chạy phần trên trong bảng điều khiển trình duyệt, chúng tôi thấy rằng asyncFunc
trả lại một lời hứa.
Hãy thực sự chia nhỏ một số mã ngay bây giờ. Hãy xem xét đoạn trích nhỏ dưới đây:
const asyncFunc = async () => {
const response = await fetch(resource);
const data = await response.json();
}
Các async
từ khóa là những gì chúng tôi sử dụng để xác định các chức năng không đồng bộ như tôi đã đề cập ở trên. Nhưng làm thế nào về await
? Chà, nó ngăn JavaScript gán fetch
vào biến phản hồi cho đến khi lời hứa được giải quyết. Khi lời hứa đã được giải quyết, giờ đây, kết quả từ phương thức tìm nạp có thể được gán cho biến phản hồi.
Điều tương tự cũng xảy ra trên dòng 3. .json
phương thức trả về một lời hứa và chúng ta có thể sử dụng await
vẫn trì hoãn việc chuyển nhượng cho đến khi lời hứa được giải quyết.
Khi tôi nói ‘đình trệ’, bạn phải nghĩ rằng việc triển khai Async và Await bằng cách nào đó chặn việc thực thi mã. Bởi vì nếu yêu cầu của chúng tôi mất quá nhiều thời gian thì sao?
Thực tế là, nó không. Mã bên trong chức năng async đang chặn, nhưng điều đó không ảnh hưởng đến việc thực thi chương trình theo bất kỳ cách nào. Việc thực thi mã của chúng tôi vẫn không đồng bộ hơn bao giờ hết. Để thể hiện điều này,
const asyncFunc = async () => {
const response = await fetch(resource);
const data = await response.json();
}
console.log(1);
cosole.log(2);
asyncFunc().then(data => console.log(data));
console.log(3);
console.log(4);
Trong bảng điều khiển trình duyệt của chúng tôi, đầu ra ở trên sẽ giống như thế này:
1
2
3
4
data returned by asyncFunc
Bạn thấy rằng khi chúng tôi gọi asyncFunc
mã của chúng tôi tiếp tục chạy cho đến khi hàm trả về kết quả.
Bài viết này không xử lý sâu các khái niệm này, nhưng tôi hy vọng nó cho bạn thấy JavaScript không đồng bộ đòi hỏi gì và một vài điều cần chú ý.
Nó là một phần rất thiết yếu của JavaScript, và bài viết này chỉ mới vạch ra bề nổi. Tuy nhiên, tôi hy vọng bài viết này đã giúp phá vỡ những khái niệm này.