- vừa được xem lúc

[JavaScript] Bài 18 - Async & Await

0 0 8

Người đăng: Semi Art

Theo Viblo Asia

Trong bài viết JavaScript số 11, chúng ta đã có một phần thảo luận ngắn về các hàm có chứa các thao tác xử lý được thực thi không đồng bộ asynchronous, và giải pháp sử dụng hàm gọi lại callback để tiếp nhận kết quả và xử lý công việc liên quan sau khi một hàm asynchronous được thực thi xong. Trong bài viết này, chúng ta sẽ cùng thảo luận chi tiết hơn về những cách thức làm việc với các thao tác xử lý được thực thi không đồng bộ.

Làm lại một ví dụ về hàm asynchronous

Ở đây chúng ta sẽ giả lập một hàm requestData gửi yêu cầu truy vấn thêm dữ liệu tới máy chủ web được thực thi không đồng bộ và có độ trễ để nhận được kết quả phản hồi từ máy chủ web là khoảng một vài giây sau khi hàm được gọi.

Kết quả trả về từ máy chủ có thể là dữ liệu data ở dạng chuỗi nếu máy chủ web xử lý thành công yêu cầu truy vấn thêm dữ liệu. Hoặc, một tín hiệu thông báo lỗi từ máy chủ web được phiên dịch thành một object Error.

Lúc này chúng ta cần thực hiện một công việc tiếp theo là cập nhật giao diện người dùng và được thực hiện bởi một hàm updateView. Do hàm requestData được thực hiện trên một tiến trình riêng, chúng ta sẽ không thể gán giá trị trả về của requestData vào một biến. Rồi sau đó truyền vào một lời gọi hàm updateView được viết song song như trong code ví dụ dưới đây.

request.js

const requestData = function() { var error, data; const mockRequest = function() { // --- nhận được dữ liệu data = 'Dữ liệu trả về từ máy chủ.' }; setTimeout(mockRequest, 2.4 * 1000); // --- trả về kết quả ở vị trí hàm được gọi return [ error, data ];
}; const updateView = function(error, data) { if (error instanceof Error) console.error(error); else console.log(data);
}; var [ data, error ] = requestData();
updateView(data, error); // 'undefined'

Lý do để dẫn tới kết quả hoạt động như trên thì chúng ta đã biết rồi. Ngay sau khi phát động lời gọi hàm requestData(), trình thực thi code không chờ đợi thao tác gửi yêu cầu đến máy chủ được thực hiện xong, mà sẽ chuyển ngay tới lời gọi hàm tiếp theo updateView(result). Và bởi vì lúc này máy chủ web vẫn chưa gửi phản hồi lại nên biến result không có chứa dữ liệu undefined.

Sử dụng hàm gọi lại callback

Để hàm updateView có thể hoạt động nối tiếp với hàm requestData thì chúng ta có thể viết lại hàm requestData nhận vào một hàm gọi lại callback để tiếp nhận dữ liệu trả về từ máy chủ và thực hiện công việc tiếp theo. Sau đó truyền hàm updateView vào vị trí callback để được thực thi trên cùng tiến trình riêng của hàm updateView.

request.js

const requestData = function(callback) { var data; const mockRequest = function() { // --- nhận được dữ liệu data = 'Dữ liệu trả về từ máy chủ.' // --- gọi hàm xử lý tiếp theo callback(null, data); }; setTimeout(mockRequest, 2.4 * 1000);
}; const updateView = function(error, data) { if (error instanceof Error) console.error(error); else console.log(data);
}; requestData(updateView);
// ...trễ 2.4 giây
// 'Dữ liệu trả về từ máy chủ.'

Bây giờ thì mọi thứ đã hoạt động như chúng ta dự kiến. Tuy nhiên chúng ta lại có một câu hỏi khác xuất hiện lúc này - Sẽ thế nào nếu như bên trong hàm updateView cũng có một thao tác xử lý khác được thực thi bất đồng bộ và chúng ta cũng cần nối tiếp thêm một hành động khác sau kết quả hoạt động của updateView?

Bởi vì lúc này hàm updateView cũng sẽ tạo ra một tiến trình thực hiện riêng khác nữa; Nếu như chúng ta muốn thực hiện thêm một hành động khác nối tiếp kết quả hoạt động của updateView thì chúng ta sẽ lại phải định nghĩa lại hàm updateView ở dạng thức tiếp nhận một hàm callback khác.

Và cứ như thế đối với trường hợp chúng ta có khoảng dăm cái thao tác xử lý bất đồng bộ cần chuyển tiếp kết quả hoạt động thì chúng ta sẽ có một mô hình các hàm callback xếp chồng trông giống như một tác phẩm nghệ thuật. Và việc theo dõi logic hoạt động của code cũng không khác lắm với chương trình đuổi hình bắt chữ. 😄

Sử dụng Promise

Để logic vận hành của trường hợp có nhiều thao tác bất đồng bộ nối tiếp được thể hiện trên bề mặt code gọn gàng, ngay ngắn, và dễ theo dõi hơn. JavaScript có cung cấp một công cụ mới để chúng ta sử dụng trong tình huống này, đó là các object Promise - Tài liệu về class Promise của MDN

promise.js

new Promise(requestData) .then((data) => updateView(data)) .then((viewData) => doNext(viewData)) .then((nextData) => doElse(nextData)) .catch((error) => handleError(error)) .finally((_) => cleanUp);

Sau khi thực hiện lời gọi hàm requestData và nhận được dữ liệu phản hồi, phương thức .then của Promise sẽ truyền dữ liệu cho hàm thực hiện thao tác xử lý tiếp theo là updateView; Kết quả hoạt động của updateView lại tiếp tục được truyền cho hàm thực hiện công việc kế tiếp doNext; Rồi sau đó kết quả hoạt động của doNext lại được chuyển tiếp cho hàm thực hiện công việc nối tiếp sau đó doElse.

Ở bất kỳ giai đoạn nào của chuỗi thao tác bất đồng bộ liên tiếp, nếu có ngoại lệ phát sinh thì tiến trình xử lý sẽ chuyển tới hàm xử lý handleError ở phương thức .catch. Sau cùng thì dù có ngoại lệ phát sinh hay không thì hàm dọn dẹp tài nguyên cleanUp ở phương thức .finally cũng sẽ được thực hiện.

Trong phương cách xử lý này, JavaScript đã định nghĩa một dạng thức chung cho các hàm truyền vào để khởi tạo Promise và có thể được nối tiếp bởi .then như sau:

promise.js

const requestData = function(resolve, reject) { // --- resolve('Dữ liệu trả về từ máy chủ.');
};

Trong đó resolve là một hàm gọi lại để thực hiện xử lý khi requestData hoàn thành công việc; Và reject là một hàm gọi lại để thực hiện xử lý khi requestData không hoàn thành được công việc và muốn thông báo lỗi.

promise.js

const requestData = function (resolve, reject) { var data; const mockRequest = function () { data = 'Dữ liệu trả về từ máy chủ.' resolve(data); }; var delay = 2.4 * 1000; setTimeout(mockRequest, delay);
};

Các hàm phía sau đó sẽ có dạng thức chung là tiếp nhận dữ liệu từ resolve() của Promise đứng liền kề phía trên để xử lý công việc. Và tiếp tục tạo ra Promise mới để resolve() chuyển cho tác vụ kế .then kế tiếp.

promise.js

const requestData = function(resolve, reject) { var data; const mockRequest = function() { data = 'Dữ liệu trả về từ máy chủ.' resolve(data); }; var delay = 2.4 * 1000; setTimeout(mockRequest, delay);
}; const updateView = function(data) { console.log(data); var viewData = 'Dữ liệu kết quả hoạt động của View.'; const update = function(resolve, reject) { resolve(viewData); }; return new Promise(update);
}; const doNext = function(viewData) { console.log(viewData); var nextData = 'Dữ liệu kết quả hoạt động của Next.'; const justDo = function (resolve, reject) { resolve(nextData); }; return new Promise(justDo);
}; const doElse = function(viewData) { console.log(viewData); const justDo = function (resolve, reject) { resolve(); }; return new Promise(justDo);
}; const cleanUp = function() { console.log('Dọn dẹp tài nguyên.');
}; new Promise(requestData) .then((data) => updateView(data)) .then((viewData) => doNext(viewData)) .then((nextData) => doElse(nextData)) .catch((error) => console.error(error)) .finally((_) => cleanUp); // kết quả:
// 'Dữ liệu trả về từ máy chủ.'
// 'Dữ liệu kết quả hoạt động của View.'
// 'Dữ liệu kết quả hoạt động của Next.'
// 'Dọn dẹp tài nguyên.'

Ở đây chúng ta cần lưu ý là tất cả những thao tác này đều đang được thực hiện trên tiến trình riêng tạo ra cho requestData và không làm trì trệ các đoạn code phía sau trong chương trình chính.

Và bởi vì các thao tác phía sau updateView, doNext, và doElse, đều chờ đợi cho thao tác liền kề phía trước thực hiện xong rồi mới bắt đầu được thực thi. Chúng ta vẫn còn một cú pháp khác giúp thể hiển sự tiếp nối của các thao tác này trong code một cách tự nhiên hơn, trông gần giống với các thao tác xử lý đồng bộ thông thường trên tiến trình chính của chương trình.

Các từ khóa asyncawait

Từ khóa await sẽ giúp chúng ta tạm dừng một tiến trình thực thi code cho đến khi một Promise được resolve và đồng thời trả về giá trị được resolve.

Tuy nhiên chúng ta cần lưu ý, tiến trình thực thi mà await được phép tạm dừng phải được chỉ định rõ - hoặc là sử dụng trong phần thân hàm của một hàm được đánh dấu là async - hoặc là được sử dụng trong scope lớn nhất trên tiến trình chính của các module.

promise.js

const requestData = function() { var data; const mockRequest = async function() { data = 'Dữ liệu trả về từ máy chủ.' var viewData = await updateView(data); var nextData = await doNext(viewData); await doElse(nextData); cleanUp(); }; var delay = 2.4 * 1000; setTimeout(mockRequest, delay);
}; /* const updateView = ... * const doNext = ... * const doElse = ... * const cleanUp = ... */ requestData(); // kết quả:
// 'Dữ liệu trả về từ máy chủ.'
// 'Dữ liệu kết quả hoạt động của View.'
// 'Dữ liệu kết quả hoạt động của Next.'
// 'Dọn dẹp tài nguyên.'

Bây giờ chúng ta thấy rằng các câu lệnh gọi các hàm xử lý tiếp theo đã có thể được viết thẳng hàng, trông rất giống với các lệnh thực thi đồng bộ với tiến trình bình thường. Và từ khóa await cũng có tính mô tả rất tốt - đó là tiến trình chạy code phụ cần phải chờ lời gọi hàm này thực thi xong đã rồi mới được thực hiện phép gán giá trị sang biến ở bên trái và đi tới câu lệnh tiếp theo bên dưới. 😄

Kết thúc bài viết

Như vậy là chúng ta đã hoàn thành xong bài viết về bộ công cụ mới, hỗ trợ chúng ta làm việc thuận tiện hơn với các tác vụ được xử lý không đồng bộ async. Tính tới thời điểm hiện tại, chúng ta đã biết tất cả các kiểu dữ liệu và các cú pháp lệnh đặc biệt của JavaScript. Mình đã dự định kết thúc Sub-Series JavaScript của Tự Học Lập Trình Web Một Cách Tự Nhiên tại đây.

Tuy nhiên thì sau khi quan sát tổng quan lại danh sách các bài viết, thì mình phát hiện ra rằng chúng ta còn thiếu 2 thứ rất quan trọng. Đó là các bài viết chuyên đề về các kiểu dữ liệu cơ bản có tần suất sử dụng nhiều như Number, String, Date, ... và các bài viết giới thiệu về các mô hình lập trình phổ biến.

Do đó nên chúng ta sẽ thực hiện thêm một vài bài viết nữa về 2 nhóm nội dung này. Mình rất hy vọng rằng bạn sẽ tiếp tục đồng hành cùng với mình trong những bài viết bổ sung của Sub-Series JavaScript. Hẹn gặp lại bạn trong bài viết tiếp theo. 😄

(Sắp đăng tải) [JavaScript] Bài 19 - Number & Math

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 525

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 433

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 153

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 145

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 110

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 245