Cách sử dụng Promise để code bất đồng bộ dễ dàng hơn (Phần 2)
JavaScript là một ngôn ngữ lập trình phía client, giúp chúng ta có những ứng dụng web đẹp hơn, thao tác dễ hơn, hiệu ứng cool hơn. Tuy nhiên, cách thức hoạt động của JavaScript hơi đặc thù một chút. Rất nhiều hoạt động của nó đều ở dạng bất đồng bộ (asynchronous). Vì vậy, việc kiểm ...
JavaScript là một ngôn ngữ lập trình phía client, giúp chúng ta có những ứng dụng web đẹp hơn, thao tác dễ hơn, hiệu ứng cool hơn. Tuy nhiên, cách thức hoạt động của JavaScript hơi đặc thù một chút. Rất nhiều hoạt động của nó đều ở dạng bất đồng bộ (asynchronous).
Vì vậy, việc kiểm soát code để nó có thể hoạt động trơn tru cũng không phải là việc đơn giản. Trong bài viết này, chúng ta sẽ tìm hiểu những phương thức mới được giới thiệu từ ECMAScript 2015 trở đi, giúp chúng ta code JavaScript bất đồng bộ được dễ dàng hơn.
Đây là phần tiếp theo của bài viết, bạn có thể xem phần 1 tại đây.
Timing
Để không gây trở ngại cũng như khó khăn cho lập trình viên, các hàm truyền qua then hay catch không bao giờ được gọi đồng bộ. Chúng cũng hoạt động hoàn toàn bất đồng bộ, sẽ được thực thi sau khi toàn bộ code đồng bộ điược thư thi hết, ngay cả trong trường hợp promise được resolve ngay lập tức.
1 2 3 4 5 6 7 |
new Promise(resolve => resolve('done')).then(() => console.log('asynchronous')); console.log('synchronous'); // Kết quả in ra sẽ là // synchronous // asynchronous |
Về mặt kỹ thuật, những hàm được truyền vào then hay catch sẽ được đưa vào một hàng đợi. Điều này giúp chúng sẽ được thực thi sau, JavaScript engine của trình duyệt sẽ bắt đầu làm việc với hàng đợi này sau khi các code đã được thực thi xong. Và tất nhiên, các hàm trong hàng đợi sẽ được lấy ra khi promise của chúng đã được resolve hoặc reject.
1 2 3 4 5 6 7 8 9 10 11 |
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait().then(() => console.log(3)); new Promise(resolve => resolve()) .then(() => console.log(2)) console.log(1); // Kết quả sẽ là // 1 // 2 // 3 |
Và promise cho chúng ta rất nhiều lợi ích so với cách sử dụng callback truyền thống ở trên:
- Callback truyền vào then, catch sẽ luôn được đảm bảo là không được thực thi khi mà các code JavaScript tiếp theo vẫn chưa hoàn thành. Trong phần lớn các trường hợp thì điều này không có nhiều ý nghĩa, tuy nhiên, nó vẫn cần thiết trong một vài trường hợp đặc biệt mà thứ tự code được thực thi sẽ cho kết quả khác nhau.
- Các callback được truyền vào then và catch luôn được gọi, kể cả trường hợp nó được thêm vào sau khi promise được settle, và sau cả một vài code đồng bộ khác. Ví dụ:
-
const x = new Promise(resolve => resolve(2)); console.log(1); x.then(console.log); // Kết quả: // 1 // 212345678const x = new Promise(resolve => resolve(2));console.log(1);x.then(console.log);// Kết quả:// 1// 2
Hoạt động của then và catch cũng là bất đồng bộ, và kết quả trả về sau khi gọi hai hàm này cũng là một promise, điều đó cho phép chúng ta có thể gọi then liên tiếp để thực hiện nhiều công việc bất đồng bộ khác nhau.
Một nhu cầu rất chính đáng của chúng ta là cần phải thực thi hai hoặc nhiều hơn các hoạt động bất đồng bộ, theo thứ tự lần lượt từng thứ kết thúc một. Trong nhiều trường hợp, chúng ta còn phải sử dụng kết quả của hành động trước để có thể tiến hành các hoạt động tiếp theo.
Promise có thể giúp chúng ta trong việc này.
1 2 3 4 |
const promise1 = doSomething(); const promise2 = promise1.then(successCallback, failureCallback); |
hoặc viết ngắn gọn hơn:
1 2 3 |
const promise2 = doSomething().then(successCallback, failureCallback); |
Như đã nói ở trên, then sẽ trả về một promise, và promise này không chỉ là doSomething đã hoàn thành mà cả successCallback cũng đã hoàn thành (có thể là failureCallback trong trường hợp lỗi). Nhờ đó, chúng ta có thể tiếp tục sử dụng promise này cho những hành động tiếp theo. Trong trường hợp đó, những callback được truyền cho promise2 sẽ cũng se được đưa vào hàng đợi, và đương nhiên, chúng phải xếp hàng ở phía sau.
Chain đơn giản với giá trị được trả về
Nói đơn giản, sau mỗi bước, một promise sẽ được trả về, và nó đại diện cho kết quả của bước đó. Cách làm này giúp chúng ta không phải truyền callback, mà sử dụng kết quả của promise để tiến hành các hoạt động tiếp theo. Lưu ý rằng, nếu muốn sử dụng kết quả của bước trước ở bước tiếp theo, chúng ta cần return kết quả đó. Để dễ hiểu hơn, hãy xem xét một ví dụ sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
new Promise(resolve => resolve(1)).then(result => { console.log(result); return result * 2; }).then(result => { console.log(result); return result * 2; }).then(result => { console.log(result); return result * 2; }).then(result => { console.log(result); }) // Kết quả sẽ là // 1 // 2 // 4 // 8 |
Hơi ngoài lề một chút, nhưng chúng ta có thể sử dụng nhiều then với cùng một promise mà không chain. Điều này là hoàn toàn hợp lệ, về mặt kỹ thuật. Nhưng khác với chain, tất cả then của cùng một promise sẽ có cùng một kết quả. Trong thực tế thì việc này không được nhiều người sử dụng, mà chain mới là thứ chúng ta cần.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const promise = new Promise(resolve => resolve(1)); promise.then(result => { console.log(result); return result * 2; }) promise.then(result => { console.log(result); return result * 2; }) promise.then(result => { console.log(result); return result * 2; }) promise.then(result => { console.log(result); }) // Kết quả: // 1 // 1 // 1 // 1 |
Return một promise
Trong ví dụ trên, chúng ta chỉ đơn giản là return một giá trị và giá trị đó được sử dụng trong chain. Nhưng chúng ta hoàn toàn có thể return một promise khác, trong trường hợp chúng ta muốn thêm hoạt động bất đồng bộ khác.
Nếu promise được return, callback được truyền vào trong then sẽ không được thực hiện ngay, mà nó sẽ phải chờ promise đó được resolve hoặc reject thì mới được thực thi. Khi đó, result của promise sẽ được sử dụng.
Quay lại với ví dụ load script của chúng ta, với mỗi hoạt động load script là một promise, và vì mỗi script lại được load bất đồng bộ nên thông thường suy nghĩ của chúng ta sẽ là thế này:
1 2 3 4 5 6 7 8 9 10 11 12 |
loadScript('script1.js').then(() => { loadScript('script2.js').then(() => { loadScript('script3.js').then(() => { loadScript('script4.js').then(() => { // Code sau khi tất cả các hoạt động bất đồng // bộ hoàn thành. }) }) }) }) |
Về cơ bản thì lại là một kim tự tháp khác thôi mà, đây là trường hợp callback không return bất cứ một kết quả gì. Nhưng với promise, thì chúng ta không cần phải làm như vậy. JavaScript đã giải quyết vấn đề này giúp chúng ta rồi. Khi then có thể trả về một promise, và khi nó trả về một promise thì promise đó phải hoàn thành thì mới tới bước tiếp theo, nên chúng ta có thể code trông rất đẹp như sau:
1 2 3 4 5 6 7 8 9 10 |
loadScript('script1.js') .then(() => loadScript('script2.js')) .then(() => loadScript('script3.js')) .then(() => loadScript('script4.js')) .then(() => { // Code sau khi tất cả các hoạt động bất đồng // bộ hoàn thành. }) |
Sử dụng promise giúp chúng ta xây dựng được chuỗi các hoạt động bất đồng bộ một cách rất dễ dàng. Ví dụ load script này có thể không thấy được kết quả ngay nên sẽ khó hình dung. Để rõ hơn, chúng ta có thể xem ví dụ sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
new Promise(resolve => setTimeout(() => resolve(1), 1000)).then(result => { console.log(result); return new Promise(resolve => setTimeout(() => resolve(result * 2), 1000)); }).then(result => { console.log(result); return new Promise(resolve => setTimeout(() => resolve(result * 2), 1000)); }).then(result => { console.log(result); return new Promise(resolve => setTimeout(() => resolve(result * 2), 1000)) }).then(result => { console.log(result); }) |
Ví dụ này có thể kiểm chứng ngay trong console của trình duyệt. Khi chạy, sau mỗi giây nó sẽ in ra một số, lần lượt sẽ là
1 2 3 4 5 6 |
1 2 4 8 |
Flow ở đây rất dễ hiểu:
- Khi promise đầu tiên được resolve sau 1 giây, nó trả về kết quả và được callback trong then sử dụng.
- Callback này in ra kết quả và trả về một promise mới.
- Vì promise mới này cũng cần 1 giây để hoàn thành, nên callback trong then tiếp theo chưa được thực thi ngay.
- Nó đợi đến khi promise được resolve mới bắt đầu thực thi, lúc nào nó in ra kết quả 2 và trả về promise mới.
- Quá trình cứ tiếp tục như vậy cho đến khi hết các callback.
Đây là cách hoạt động của promise chain, nó cho chúng ta code chuỗi các hoạt động bất đồng bộ một cách dễ dàng mà không phải sử dụng code kiểu lồng nhau, nguy cơ rất lớn dẫn đến callback hell. Có thể thấy đây chính là điểm mấu chốt của promise chain. Chúng ta cứ tạm hiểu rằng, return một giá trị xác định là một biến thể của hình thức này, khi đó JavaScript sẽ “ngầm” tạo ra một promise được resolve ngay với giá trị đó.
Xử lý khi gặp lỗi
Trong những ví dụ ở trên, chúng ta mới chỉ quan tâm đến việc các hoạt động bất đồng bộ kết thúc thành công. Tuy nhiên, không thể đảm bảo rằng, tất cả chúng sẽ luôn thành công như vậy. Trong trường hợp lỗi, tất nhiên là promise sẽ bị reject thay vì resolve.
Và một nhu cầu tất yếu (dù không thường xuyên) là cần phải giải quyết hậu quả khi có lỗi xảy ra. Promise chain có một cơ chế tuyệt vời giúp chúng ta làm việc đó.
1 2 3 4 5 6 7 8 9 |
doSomething() .then(result => doSomethingEles()) .then(newResult => doOtherThing()) .then(finalResult => { console.log(`I got the final result: ${finalResult}`) }) .catch(failureCallback) |
Promise chain có một cơ chế rất hay, khi một promise bị reject, ngay lập tức, nó sẽ thực thi code xử lý gần nhất trong chuỗi. Cách làm này tương tự như cơ chế try...catch thông thường.