Mọi ngôn ngữ lập trình đều không hoàn hảo, sẽ luôn có những khuyết điểm và khó khăn khi học cũng như khi sử dụng một ngôn ngữ nào đó. Một trong những ngôn ngữ lập trình lớn và được nhiều người dùng nhất hiện nay: JavaScript đã phải trải qua một khoảng thời gian dài khó khăn khi phải dựa vào callbacks để lập trình các đoạn code bất đồng bộ. Sau đó trong bản cập nhật ES7, JavaScript đã đưa ra một giải pháp mới được cộng đồng lập trình rất ủng hộ để giải quyết cái bài toàn bất đồng bộ chính là Async và Await
Tổng quan về sự ra đời của Async, Await
Callbacks
Callbacks là giải pháp đầu tiên của các JavaScript Developer sử dụng để giải quyết các thuật toán bất đồng bộ tuy nhiên Callbacks có quá nhiều vấn đề khi áp dụng thực tế như:
- Thời gian vận hành rất lâu khi các đoạn code callbacks phải chờ nhau để thực hiện chức năng
- Những đoạn code lồng ghép và chồng chéo lên nhau gây nhiều khó khăn trong việc bảo trì, sửa chữa cũng như nâng cấp, phát triển phần mềm
Promise
Những khó khăn của các lập trình viên Javascript được giải quyết nhờ một sự thay thế tuyệt vời cho callbacks chính là Promise được cập nhật thêm trong phiên bản ES6. Kể từ khi được ra mắt Promise đã được cộng đồng ủng hộ mạnh mẽ và vui vẻ chuyển sang dùng sau thời gian bị đày đọa quá lâu bởi callbacks. Promise cải tiến được một phần và biểu hiện rõ nhất là cú pháp đơn giản, dễ quản lý hơn tuy nhiên vấn đề còn tồn đọng lại là thời gian xử lý thuật toán vẫn còn rất mất thời gian vì bản chất của Promise vẫn là thao tác bất đồng bộ và vẫn phải chờ nhau để hoàn thành request.
Async/Await
Và cuối cùng vào phiên bản ES7 của Javascript, các lập trình viên đã được tiếp cận với phương thức mới là Async và Await. Async/Await là một bản nâng cấp khá toàn diện và giải quyết được vấn đề mà Promise chưa làm được đó là tối ưu thời gian xử lý.
Async/await đã trải qua khoảng thời gian khá dài để xuất hiện kể từ khi JS bắt đầu phát triển và giải quyết vấn đề về lập trình bất đồng bộ để rồi cho ra mắt một phương thức khá toàn diện về mặt kĩ thuật và cả mỹ thuật giúp cho việc xây dựng ứng dụng, bảo trì và nâng cấp code cũng dễ dàng hơn rất nhiều.
Kiến thức khái quát và phương thức hoạt động của async/await
Generator
Async/await hoạt động dựa trên nền tảng Generator nên để có thể làm việc tốt trên async/await thì bạn phải có những kiến thức và hiểu biết cơ bản về generator trước.
Javascipt giới thiệu Generator ở bản cập nhật ES6- bản cập nhật có sự xuất hiện lần đầu của Promise. Generator trong Javascipt được hiểu đơn giản là một chức năng function*. Khi sử dụng function* bạn sẽ nhận được một generator ( nằm trong lớp GeneratorFunction) thay cho một hàm bình thường.
Hoặc người dùng có thể sử dụng generator expression để quy định:
Vì chúng ta không thể truy cập được đối tượng thuộc lớp Generator Function nên các bạn nên dùng cú pháp như trên:
Cách thức hoạt động
Khi một hàm Generator được gọi ra sử dụng thì nó sẽ không chạy ngay mà sẽ trả về đối tượng là iterator. Generator chỉ bắt đầu thực thi khi hàm next của interator được gọi và sẽ ngưng thực thi khi gặp lệnh yield. Kết quả trả về của next sẽ là những gì diễn ra sau lệnh yield.
Kết quả trả về cho phương thức next là một đối tượng mà giá trị đã “được yield” và một trạng thái thông báo cho biết là Generator đã yield kết quả cuối cùng chưa ( trạng thái của kết quả là done, true hoặc false).
Generator cũng có thể sử dụng hàm yield* để lại… yield một hàm generator khác. Bạn cũng có thể áp dụng return cho generator và giá trị trả về của return cũng sẽ như yield là trả về giá trị của next. Sau khi thực hiện xong thì hàm cũng sẽ kết thúc.
Một đặc điểm đáng chú ý của Generator là khi hàm đã kết thúc thì sẽ không tiếp tục chạy tiếp nữa cho dù chúng ta có gọi thêm next. Nếu có sự cố exception thì generator sẽ ngưng và hiện trạng thái là done nhưng giá trị trả về là undefined
Với phương thức hoạt động mới của mình, Generator rất thích hợp để giải quyết những bài toán ngốn nhiều thời gian và tài nguyên bộ nhớ vì Generator chỉ tính toán và chỉ yield giá trị khi cần.
Ví dụ về các loại generator
Generator thường
Expression trong Generator
Dùng yield* để gọi hàm generator khác
Nhập tham số vào hàm Generator
Trạng thái của generator có thể được thay đổi bằng cách truyền tham số vào phương thức next. Giá trị mà next nhận sẽ được xem như kết quả cuối cùng được trả về của lệnh yield trước khi Generator tạm ngưng.
Đây là ví dụ trong việc sử dụng next(true) để reset lại chuỗi:
Sử dụng return trong hàm Generator
Sử dụng generator để code bất đồng bộ
Generator được nhiều người thích dùng để code bất đồng bộ là nhờ đặc điểm của hàm generator có thể thực thi và tạm ngưng bất cừ lúc nào và mọi trạng thái của nó sẽ được lưu trữ lại. Tất cả những biến số, hằng số, tính chất… sẽ được trả về giá trị chính xác khi hàm tiếp tục chạy sau khi tạm ngưng.
Nhờ đặc điểm đó Generator có thể cùng Promise giúp các lập trình viên java giải quyết các bài toán bất đồng bộ dễ hơn. Promise như đã nói như trên đã giải quyết được vấn đề khó đọc tuy nhiên vẫn còn dùng đến phương thức callback.
Hiện nay thì chúng ta có thể kết hợp cả Generator và Promise vào bài toán bất đồng bộ bằng cách: mỗi lần tạm dừng sẽ yield một promise và khi tiếp tục chạy thì hàm next trong callback của promise đó sẽ yêu cầu hàm tiếp tục chạy. Sau đây là một đoạn code ví dụ:
Code sau nhiều lần thay đổi đã đơn giản và dễ đọc hơn nhiều so với callback và nó hoạt động rất chính xác theo chuỗi hành động sau:
- In before async
- –1s sau–
- In kết quả promise
- In after async
Đây là cách kết hợp Generator và Promise để ta có thể tạm ngưng hàm chờ các đoạn bất đồng bộ và sau đó cho hàm tiếp tục chạy.
Với cách hoạt động này cho thấy kết quả khả quan hơn nhiều so với Promise vì dùng Promise thì code bất đồng bộ lúc nào cũng sẽ thực thi sau các code đồng bộ.
Generator cũng có thể xử lý lỗi bằng cú pháp truyền thống của JavaScript: try…catch.
Sau đây là một ví dụ khá tổng quát cho việc tạm ngưng code với Generator và Promise:
Hàm này sẽ dừng một hàm bằng phương pháp yield một Promise sau đó chờ hàm kết thúc và nhận kết quả trả về:
Để cho ngưng hàm liên tục thì chúng ta có thể sử dụng phương pháp đệ quy:
Đây là một cách tổng quát để sử dụng code bất đồng bộ. Tuy nhiên với một số trường hợp khác thường thì cần phải có sự tinh chỉnh nhất định. Tổ chức Ecma Internatina với đặc tả ECMA Script đã giúp chúng ta giải quyết vấn đề này.
Async/await
Async/await được bao gồm trong đặc tả ECMAScript2017 (ES 8) nên không phải tất cả các trình duyệt đều hỗ trợ. Bạn có thể cài đặt babel-preset-env để có thể áp dụng async, await cho trình duyệt bản cũ.
Với ý tưởng tựa như hàm async tổng quát ở phần trước, async/await được xây dựng trên nền tảng Generator và Promise tuy nhiên cấu trúc phức tạp và khái quát hơn.
Cách áp dụng cho async/await vào các bài toán bất đồng bộ
Hàm async
Định nghĩa hàm bất đồng bộ bằng async
Định nghĩa hàm bất đồng bộ bằng function expression
Định nghĩa hàm bất đồng bộ bằng cách kết hợp với cú pháp arrow function của ES 6
Chúng ta có thể định nghĩa một hàm thuộc lớp AsyncFunction bằng async vì Async Function không thể truy cập như biến toàn cục mà phải dùng đến async.
AsyncFunction luôn return về một Promise nếu trong code không trả về Promise nào thì sẽ có một Promise mới được resolve với giá trị return lúc đầu (kết quả sẽ trả về undefined nếu không có giá trị nào trong return). Vì kết quả trả về là promise nên bạn cũng có thể dùng cùng với callback như promise bình thường:
Nếu async trả về luôn một promise thì ta không cần resolve nữa:
Qua 2 ví dụ trên ta có thể thấy rằng async luôn trả về kết quả là promise và async kết hợp với await sẽ giúp chúng ta có những ứng dụng tuyệt vời hơn nữa.
Await
Await là lệnh ngưng việc chạy code trong hàm async cho đến khi promise được resolve thì code mới tiếp tục được chạy với giá trị resolve vừa nhận được. Một lưu ý quan trọng là Await chỉ dùng được bên trong async.
Giờ đây bạn đã có thể code bất đồng bộ dễ dàng hơn nhiều khi kết hợp giữa promise với callback, async,await cùng generator.
Kết quả của đoạn code trên sẽ là:
- –sau 1s—
- Console in ra 1
Hàm async đã bị tạm dừng khi gặp await và phải dừng 1 giây trước khi chạy tiếp để chờ kết quả được resolve từ promise.
Với phương thức này chúng ta có thể code bất đồng bộ dễ dàng hơn mà không cần callback. Ngoài ra chúng ta có thể dùng lại await nhiều lần nếu muốn canh chỉnh cho các hoạt động bất đồng bộ diễn ra lần lượt.
Loadscript sử dụng async/await:
Trong trường hợp sử dụng await không có promise thì await được xem như là một giá trị, một promise đã được resolve với giá trị đó.
Các lệnh được code sau await luôn được chạy sau các đoạn code đồng bộ khác
Phương thức hoạt động của await là sẽ chờ lấy kết quả promise đã được resolve và trả về kết quả đó. Nhưng nếu gặp lỗi promise bị từ chối, kết quả trả về sẽ là một exception.
Cách thức hoạt động của async
Chúng ta có thể áp dụng async bằng cách chèn thêm async vào trước một class nên về mặt cơ bản thì cách hoạt động của async không khác một hàm nhiều lắm.
Tuy có giống nhau về ý tưởng nhưng do cách thiết kế khác nhau nên async/await hoạt động khác xa với promise chain. Với quy tắc hoạt động ngưng- chạy của async/await có thể khiến 2 promise cùng chạy một lúc trong một số trường hợp:
Theo ví dụ trên thì dù 2 promise slow và fast đều được chạy nhưng await đã cho ngưng lại cả 2 và chỉ cho chạy tiếp khi promise đó đã resolve. Vậy nên đoạn code sẽ mất 2 giây để chạy và 2 kết quả của slow và fast sẽ xuất hiện liền nhau.
Với trường hợp cần những hoạt động song song như thế này thì Promise.then có thể đem lại sự đơn giản và hiệu quả hơn so với await vì khi đó promise nào xong trước sẽ hiện trước
Nâng cấp promise chain thành async/await
Hiện nay chúng ta có thể làm việc với fetch API của JavaScript khi API này sẽ gửi truy vấn đến 1 URL và nhận lại kết quả là một promise. Promise được trả về đó có thể được dùng bởi promise chain:
Đoạn code trên có thể áp dụng async/await như sau:
Xử lý lỗi
Kết quả bình thường khi bạn sử dụng async/await là một kết quả đã được resolve. Nhưng khi promise bị reject và exception xảy ra thì đây là một ví dụ:
Đoạn code 1:
Đoạn code 2:
Cả 2 sẽ đều cho ra kết quả như sau
Chúng ta có thể dùng try…catch để giải quyết các lỗi này như các hàm bình thường khác:
Trong một số tình huống, Promise sẽ chạy trong một khoảng thời gian rồi mới bị reject và exception mới xuất hiện.
Với hàm async, bạn cũng có thể áp dụng try…catch, khi có exception xảy ra, catch gần nhất sẽ được dùng để giải quyết. Cũng như Promise, async cũng không thể sử dụng try…catch bên ngoài hàm được.
Ngoài ra bạn còn có thể sử dụng callback với Promise.catch hoặc Promise.then(null,function) nếu muốn giải quyết exception ở ngoài hàm async. Vì kết quả trả về của async là một promise nên đây cũng là một cách giải quyết tốt
Kết luận
Sự bổ sung của cặp async/await đã đem đến cho JavaScript một sự nâng cấp mạnh mẽ giúp người dùng thuận tiện hơn trong việc viết code, bảo trì và xử lý các bài toán không đồng bộ, callback…