Xem thêm

Tổng hợp xử lý bất đồng bộ trong Javascript: callback, promise và async

Huy Erick
Lúc trước khi đi phỏng vấn, tôi được đặt câu hỏi là tên của các phương thức xử lý bất đồng bộ trong JavaScript. Tuy chỉ nhớ được async/await vì đã từng đọc qua ở...

Lúc trước khi đi phỏng vấn, tôi được đặt câu hỏi là tên của các phương thức xử lý bất đồng bộ trong JavaScript. Tuy chỉ nhớ được async/await vì đã từng đọc qua ở đâu đó, nhưng lại không nhớ chính xác cú pháp. Vì vậy, sau buổi hôm đó, tôi quyết định tìm hiểu về các phương thức xử lý bất đồng bộ trong JavaScript và các vấn đề liên quan. Hôm nay, tôi xin viết một bài viết về chủ đề này.

Đồng bộ và bất đồng bộ trong JavaScript

Trước khi đi sâu vào vấn đề, hãy cùng tìm hiểu về khái niệm đồng bộ và bất đồng bộ. Đồng bộ có nghĩa là thực hiện các công việc một cách tuần tự. Công việc A phải hoàn thành trước mới đến công việc B. Điều này ảnh hưởng đến hiệu suất của ứng dụng. Ví dụ, nếu một request gửi lên server yêu cầu server thực hiện chức năng như import file hoặc đọc ghi file, thì server sẽ mất nhiều thời gian để xử lý công việc này. Trong thời gian đó, server sẽ không thể thực hiện bất kỳ hành động nào khác. Điều này cũng có thể gây crash server nếu có nhiều request được gửi đến trong khi server chưa thực hiện xong những công việc trước đó.

Để khắc phục tình trạng này, các ngôn ngữ lập trình như C/ c+ +, Java,... sử dụng cơ chế đa luồng (multi-thread). Mỗi công việc tốn thời gian sẽ được thực hiện trên một thread riêng biệt mà không can thiệp vào thread chính. Tuy nhiên, JavaScript là một ngôn ngữ Single thread, tức là chỉ có thể sử dụng 1 luồng duy nhất. Đó là lý do tại sao chúng ta phải sử dụng cơ chế xử lý bất đồng bộ.

Tổng hợp xử lý bất đồng bộ trong Javascript: callback, promise và async

Cơ chế xử lý bất đồng bộ trong JavaScript

Với JavaScript, chúng ta có thể sử dụng các cơ chế xử lý bất đồng bộ như Callback, Promise và Async/await.

Callback

Callback là một đoạn mã được truyền như một tham số của một hàm và chờ để được gọi vào thực thi. Ví dụ:

console.log("Hello, "); setTimeout(function() {     console.log("world!"); }, 1000);

Ở ví dụ trên, chúng ta mong đợi hành động đầu tiên là in ra màn hình chữ "Hello, world", ngay sau khi in xong thì tiếp tục in tiếp dòng chữ "My name is...". Kết quả sẽ như sau:

Hello, My name is... world!

Callback cũng được sử dụng khi chúng ta sử dụng AJAX. Khi gửi request đến server thành công và server trả về kết quả thành công, một hàm success sẽ được gọi. Sau đó, chúng ta có thể viết mã để xử lý callback này.

Tuy callback đã giải quyết một phần vấn đề của việc xử lý bất đồng bộ, nhưng nếu có quá nhiều callback được lồng vào nhau, sẽ rất khó hiểu và khó maintain. Điều này được gọi là "Callback hell" và chúng ta cần tránh điều này khi lập trình. Có nhiều cách để phòng chống callback hell, nhưng tôi sẽ chỉ ra một vài cách phổ biến.

Promise

Promise là một cơ chế trong JavaScript giúp bạn thực thi các tác vụ bất đồng bộ mà không rơi vào callback hell hay "pyramid of doom", tức là tình trạng các hàm callback lồng vào nhau ở quá nhiều tầng. Các tác vụ bất đồng bộ có thể là gửi AJAX request, gọi hàm bên trong setTimeout, setInterval hoặc requestAnimationFrame, thao tác với WebSocket hoặc Worker, v.v...

Promise giúp chúng ta giải quyết câu hỏi "Nếu thành công thì làm gì? Nếu thất bại thì làm gì?" bằng cách xử lý kết quả của một hành động cụ thể. Kết quả của mỗi hành động sẽ là thành công hoặc thất bại, và Promise sẽ giúp chúng ta xử lý kết quả này.

Một Promise có thể ở một trong ba trạng thái: Fulfilled (hoàn thành và thành công), Rejected (hoàn thành nhưng thất bại), hoặc Pending (đang chờ xử lý hoặc bị từ chối). Hai trạng thái Fulfilled và Rejected được gọi là Settled, tức là đã xử lý xong.

Cách tạo một Promise

Để tạo một Promise, chúng ta sử dụng cú pháp sau:

var promise = new Promise(callback);

Trong đó, callback là một hàm có hai tham số truyền vào như sau:

var promise = new Promise(function(resolve, reject) {     // Thực hiện công việc và gọi hàm resolve khi hoàn thành thành công     // hoặc gọi hàm reject khi hoàn thành thất bại });

Dưới đây là một ví dụ minh họa sử dụng Promise để đọc file:

const fs = require("fs");  function readFile(filename) {     return new Promise(function(resolve, reject) {         fs.readFile(filename, "utf8", function(err, data) {             if (err) {                 reject(err);             } else {                 resolve(data);             }         });     }); }  readFile("file1.txt")     .then(function(data1) {         console.log(data1);         return readFile("file2.txt");     })     .then(function(data2) {         console.log(data2);     })     .catch(function(error) {         console.error(error);     });

Ở ví dụ trên, chúng ta sử dụng một module của Node.js để đọc file. Công việc mà chúng tôi muốn thực hiện là sau khi đọc xong nội dung của file 1 và in ra nội dung đó, chúng ta mới được tiến hành đọc nội dung của file 2. Chúng ta đã xử lý bằng cách truyền các callback vào lần lượt trong từng hàm then. Lưu ý rằng, để hàm then phía sau có thể thực hiện được, callback trong hàm then trước đó phải trả về một Promise.

Promise đã giải quyết tốt vấn đề của callback. Tuy nhiên, việc sử dụng Promise cũng có nhược điểm như phải truyền callback vào hàm thencatch, làm mã nguồn trở nên dư thừa và khó debug, vì toàn bộ các hàm then chỉ được tính là một câu lệnh duy nhất.

Async/await

Khi ES7 ra đời, có một tính năng mới được giới thiệu là async/await, giúp giải quyết vấn đề của Promise. Để sử dụng async/await, chúng ta cần khai báo từ khóa async trước từ khóa định nghĩa hàm. Ví dụ:

async function myFunction() {     // Code xử lý bất đồng bộ }

Với từ khóa async, chúng ta có thể đợi các Promise xử lý trong hàm đó mà không tạm dùng luồng chính bằng từ khóa await. Ví dụ:

async function myFunction() {     console.log("Hello, ");     await new Promise((resolve) => setTimeout(resolve, 1000));     console.log("world!"); }  myFunction();

Kết quả trả về từ hàm async luôn là một Promise, dù bạn có sử dụng await (thực hiện xử lý bất đồng bộ) hay không. Promise này sẽ ở trạng thái thành công với kết quả được trả về bằng từ khóa return của hàm async, hoặc ở trạng thái thất bại với kết quả được đẩy qua từ khóa throw trong hàm async. Bản chất của hàm async chính là Promise.

Với Promise, chúng ta có thể xử lý ngoại lệ bằng cách sử dụng catch. Tuy nhiên, việc này không dễ dàng theo dõi và đọc. Nhưng với async/await, việc này trở nên cực kì đơn giản bằng từ khóa try/catch giống như các thao tác đồng bộ.

Tóm tắt

Tóm lại, các phương thức xử lý bất đồng bộ trong JavaScript như Callback, Promise và Async/await đều giúp chúng ta xử lý các công việc bất đồng bộ một cách hiệu quả. async/await được giới thiệu trong ES7 như một sự cải tiến của Promise, giúp mã nguồn trở nên dễ đọc và dễ hiểu hơn.

Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về các phương thức xử lý bất đồng bộ trong JavaScript và sẽ giúp bạn áp dụng chúng vào công việc lập trình của mình.

Tổng hợp xử lý bất đồng bộ trong Javascript: callback, promise và async

1