[JavaScript] Bài 18 – Async & Await

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

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

constrequestData=function(){var error, data;constmockRequest=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ọireturn[ error, data ];};constupdateView=function(error, data){if(error instanceofError)
      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

constrequestData=function(callback){var data;constmockRequest=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 theocallback(null, data);};setTimeout(mockRequest,2.4*1000);};constupdateView=function(error, data){if(error instanceofError)
      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 PromiseTà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

constrequestData=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

constrequestData=function(resolve, reject){var data;constmockRequest=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

constrequestData=function(resolve, reject){var data;constmockRequest=function(){
      data ='Dữ liệu trả về từ máy chủ.'resolve(data);};var delay =2.4*1000;setTimeout(mockRequest, delay);};constupdateView=function(data){
   console.log(data);var viewData ='Dữ liệu kết quả hoạt động của View.';constupdate=function(resolve, reject){resolve(viewData);};returnnewPromise(update);};constdoNext=function(viewData){
   console.log(viewData);var nextData ='Dữ liệu kết quả hoạt động của Next.';constjustDo=function(resolve, reject){resolve(nextData);};returnnewPromise(justDo);};constdoElse=function(viewData){
   console.log(viewData);constjustDo=function(resolve, reject){resolve();};returnnewPromise(justDo);};constcleanUp=function(){
   console.log('Dọn dẹp tài nguyên.');};newPromise(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

constrequestData=function(){var data;constmockRequest=asyncfunction(){
      data ='Dữ liệu trả về từ máy chủ.'var viewData =awaitupdateView(data);var nextData =awaitdoNext(viewData);awaitdoElse(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

Nguồn: viblo.asia

Bài viết liên quan

Thay đổi Package Name của Android Studio dể dàng với plugin APR

Nếu bạn đang gặp khó khăn hoặc bế tắc trong việc thay đổi package name trong And

Lỗi không Update Meta_Value Khi thay thế hình ảnh cũ bằng hình ảnh mới trong WordPress

Mã dưới đây hoạt động tốt có 1 lỗi không update được postmeta ” meta_key=

Bài 1 – React Native DevOps các khái niệm và các cài đặt căn bản

Hướng dẫn setup jenkins agent để bắt đầu build mobile bằng jenkins cho devloper an t

Chuyển đổi từ monolith sang microservices qua ví dụ

1. Why microservices? Microservices là kiến trúc hệ thống phần mềm hướng dịch vụ,