Cú pháp Async/Await trong Node.js
Cú pháp Async/Await được xem là tính năng lớn nhất khi các nhà phát triển cho ra mắt Node.js 7.6.0 => Cú pháp thực hiện các công việc bất đồng bộ “giống như” đồng bộ bằng từ khóa Async/Await đã được hỗ trợ chính thức mà không cần các tùy chọn thử nghiệm khi chạy ứng dụng Nodejs.
Callback-hell hay Promise-hell giờ có thể coi là quá khứ. Nhưng, sức mạnh và sự tiện dụng luôn đi kèm với những vấn đề không lường trước được – Nhất là đối với những người mới.
Nếu bạn không hiểu rõ vấn đề, từ khóa Async/Await sẽ làm bạn bối rối trong một vài trường hợp, và sau một thời gian bạn có thể gặp Async/Await-hell.
Toàn bộ code trong bài viết này được chạy với Nodejs 7.6.0 (mình dùng nvm để quản lý nhiều node version trên một máy tính), chúng sẽ không chạy với các phiên bản thấp hơn.
Nodejs 7.x là một bản phát hành với số version là số lẻ, version này được lên kế hoạch ngừng hộ trợ từ tháng 6 năm 2017, vì vậy tôi khuyên bạn không nên dùng phiên bản này cho các ứng dụng production.
Hello World!
Đây là 1 ví dụ đơn giản “Hello World!” sử dụng cú pháp Async/Await:
async function hello() { await new Promise(resolve => setTimeout(resolve, 1000)); console.log('Hello World!'); } hello();
Bạn có thể chạy file script có nội dung như trên giống như những file js khác mà không cần tới các bộ chuyển đổi (như typescript, babel...
), nó sẽ in ra màn hình "Hello World!"
sau khoảng 1 giây.
~/Desktop via ⬢ v7.6.0 ➜ node test.js Hello World! ~/Desktop via ⬢ v7.6.0 ➜ time node test.js Hello World! node test.js 0.08s user 0.02s system 8% cpu 1.099 total
Async function dựa trên cơ sở của Promise(hay các thenable object) để hoạt động. Bạn nên sử dụng từ khóa await với một Promise. Nếu bạn bạn sử dụng với một đối tượng không phải là Promise (hay thenable object), sẽ không có chuyện gì xảy ra, mọi thứ hoạt động như không có từ khóa await.
async function hello() { // Vẫn hoạt động, nhưng từ khóa await không có ý nghĩa gì cả await 5; console.log('Hello World!'); } hello();
Cú pháp Async/Await không chỉ hoạt động với Promise mặc định của Nodejs, chúng có thể hoạt động với Bluebird hay những thư viện Promise khác.
Tóm lại, chúng hoạt động với những thenable object (những object có hàm .then()
)
async function hello() { // Đối tượng này là một "thenable". // nó có một phần giống như một Promise // nhưng không phải là một Promise await { then: resolve => setTimeout(resolve, 1000) }; console.log('Hello World!'); } hello();
Có một điều quan trọng khi sử dụng từ khóa await: Bạn phải sử dụng await ở trong một function, mà function đó được khai báo với từ khóa async. Đoạn code dưới đây sẽ bị lỗi cú pháp:
function hello() { const p = new Promise(resolve => setTimeout(resolve, 1000)); // SyntaxError: Unexpected identifier await p; } hello();
Hơn nữa, từ khóa await cũng không thể sử dụng được nếu function scope của nó không phải là một async function. Đoạn code dưới đây sẽ bị lỗi cú pháp:
async function hello() { const p = Promise.resolve('test'); otherLib.doAnything(function() { // SyntaxError: Unexpected identifier await p; }); console.log('Hello, world!'); } hello();
Một điều cực kỳ quan trọng nữa khi sử dụng Async/Await, một async function luôn luôn trả lại một Promise.
async function hello() { await new Promise(resolve => setTimeout(resolve, 1000)); console.log('Hello World!'); } const result = hello(); // Prints "Promise { <pending> }" console.log(result);
Điều đó có nghĩa là bạn có thể await với một kết quả trả về của async function => đây là trường hợp sử dụng chủ yếu.
async function wait(ms) { await new Promise(resolve => setTimeout(resolve, ms)); } async function hello() { // Vì `wait()` được đánh dấu là một `async` function, kết quả trả về của hàm sẽ là một Promise, nên // bạn có thể `await` await wait(1000); console.log('Hello, World!'); } hello();
Lấy kết quả trả lại và xử lý lỗi một async function
Như chúng ta biết thì một Promise có thể resolve với một kết quả hoặc reject với một lỗi. Async/Await cho phép chúng ta xử lý các trường hợp này giống như xử lý các công việc đồng bộ: Gán kết quả của resolve, và try/catch
để bắt các lỗi. Kết quả trả của một await là kết quả được trả lại trong resolve bởi Promise:
async function hello() { const res = await new Promise(resolve => { // Promise này sẽ trả lại string "Hello World!" sau khoảng 1 giây setTimeout(() => resolve('Hello World!'), 1000); }); // Print "Hello World!". `res` tương đương kết quả được trả lại bởi Promise bằng resolve console.log(res); } hello();
Trong một async function, bạn có thể sử dụng try/catch
để bắt những lỗi được trả ra bởi Promise thông qua reject (hoặc những lỗi không mong muốn). Nói một cách khác, việc xử lý các công việc bất đồng bộ sẽ giống như xử lý các công việc đồng bộ:
async function hello() { try { await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('OOPS!')), 1000); }); } catch (error) { // Prints "Caught OOPS!" console.log('Caught', error.message); } } hello();
Sử dụng try/catch để xử lý lỗi là một cơ chế rất tiện dụng bởi vì nó cho phép bạn xử lý nhiều lỗi đồng bộ và bất đồng bộ với một cú pháp duy nhất
function badSync() { throw new Error('badSync'); } function badAsync() { return new Promise(() => { throw new Error('badAsync'); }); } async function hello() { try { await badSync(); } catch (error) { console.log('caught', error.message); } try { await badAsync(); } catch (error) { console.log('caught', error.message); } } hello();
Vòng lặp và điều kiện
Một trong những điểm nổi bật tiếp theo của Async/Await là bạn có thể viết code xử lý bất động bộ có sử dụng if, for như khi bạn xử lý với các công việc đồng bộ, điều mà bạn chắc hẳn đã gặp rất nhiều khó khăn khi làm việc với các hàm Callback
. Bạn không cần thêm các thư viện hỗ trợ như async , bạn chỉ cần sử dụng cấu trúc rẽ nhánh và vòng lặp như cách bạn vẫn dùng. Đây là một ví dụ sử dụng vòng lặp for:
function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function hello() { for (let i = 0; i < 10; ++i) { await wait(1000);
// In ra "Hello World!" 10 lần, sau mỗi giây và thoát console.log('Hello World!'); } } hello();
Và một ví dụ có sử dụng if:
function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function test() { for (let i = 0; i < 10; ++i) { if (i < 5) { await wait(1000); } // In ra "Hello World!" 5 lần sau mỗi giây, sau đó in ra 5 lần "Hello World!" ngay lập tức console.log('Hello World!'); } } test();
Hãy nhớ nó vẫn luôn là bất đồng bộ
Một trong những câu hỏi phỏng vấn cho vị trí lập trình viên Javascript mà tôi thường dùng nhất là: Đoạn code sau sẽ in ra những gì? (tất nhiên là không có chú thích :D)
for (var i = 0; i < 5; ++i) { // Thực tế sẽ in ra 5 lần "5" // Nếu thay var bằng let, nó sẽ in ra từ 0 cho tới 4 setTimeout(() => console.log(i), 0); } // Đoạn này sẽ được in ra trước 5 giây console.log('end');
Những đoạn code bất đồng bộ dễ gây nhầm lẫn, nhưng với async/await việc viết code bất đồng bộ sẽ dễ dàng hơn rất nhiều mà không làm mất đi bản chất của code bất đồng bộ. Chỉ bởi vì async trông giống code đồng bộ, nhưng thực ra thì không phải như vậy:
function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function hello(ms) { for (let i = 0; i < 5; ++i) { await wait(ms); console.log(ms * (i + 1)); } } // 2 lệnh gọi này sẽ thực hiện gần như là "song song" hello(30); hello(70);
Kết quả in có thể là:
30 60 70 90 120 140 150 210 280 350
Kết luận
Async/Await là một thứ vô cùng mạnh mẽ, giúp những đoạn code Javascript của chúng ta trở nên dễ đọc hơn rất nhiều, nhưng nó cũng luôn kèm theo những rủi ro mà bạn khó lường trường được.
Cá nhân mình cực kỳ thích cú pháp này, và mình khuyên các bạn hãy áp dụng nó vào các dự án của mình. Để tận dụng được điểm mạnh của nó, cách tốt nhất là bạn phải thật hiểu chúng.
Trong bài viết này cũng không đề cập tới việc sử dụng async/await trong những trường hợp đặc biệt như kết hợp với các function xử lý mảng .map, .forEach, .reduce
hay các xử lý song song…Đó là những phần tương đối phức tạp, mình sẽ tách thành một bài viết khác.