10/12/2018, 19:19

Cách sử dụng Promise để code bất đồng bộ dễ dàng hơn (Phần 1)

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.

Callback là tên mà chúng ta dùng để gọi các hàm JavaScript trong một trường hợp đặc biệt. Rất khó để định nghĩa chúng nhưng có thể rất dễ hiểu thông qua ví dụ dưới đây. Callback chỉ là tên được cộng động dùng, nó không có gì đặc biệt trong ngôn ngữ này cả.

Thuật ngữ bất đồng bộ (asynchronous, hoặc gọi ngắn là async) có thể hiểu rằng “sẽ mất một chút thời gian”, “sẽ hoàn thành trong tương lai, không phải bây giờ”. Callback là phương án được sử dụng phổ biến trong những hoạt động bất đồng bộ này.

Hoạt động bất đồng bộ của JavaScript diễn ra rất thường xuyên. Là một lập trình viên web, chắc hẳn bạn đã rất quen thuộc với những truy vấn kiểu ajax. Chúng ta có thể xem xét một ví dụ thực tế như sau:

Mục đích của hàm trên là để load một file JavaScript bằng JavaScript. Sau khi chạy hàm này, nó sẽ chèn thêm một thẻ <script src="${src}"></script> vào trong head, sau đó trình duyệt sẽ tải file này về và thực thi.

Cách sử dụng nó rất đơn giản:

Hàm này hoạt động một cách bất đồng bộ, bởi vì việc tải file script sẽ mất một chút thời gian. Việc gọi hàm sẽ bất đầu việc load script, việc load này sẽ được trình duyệt thực hiện “ngầm” bởi một tiến trình khác. Những code phía dưới hàm này sẽ tiếp tục được thực thi mà không cần đợi script được load. Thâm chí, nó có thể kết thúc trước cả việc script được load xong.

Việc hoạt động bất đồng bộ này không phải là vấn đề, chúng ta hoàn toàn không cần quan tâm. Tuy nhiên, có một vài trường hợp, khi load script mới, nó định nghĩa một số hàm và biến, và chúng ta cần sử dụng lại những thứ này. Điều này thường gặp khi chúng ta sử dụng các thư viện, như jQuery chẳng hạn:

Rất tự nhiên, trình duyệt sẽ cần thời gian để tải thư viện jQuery về. Tuy nhiên, nó lại không chờ cho script được tải về mà sẽ ngay lập tức thực hiện lệnh tiếp theo.

Vì vậy, những code tiếp theo sẽ không thực thi được mà chúng ta sẽ gặp lỗi:

Với cách làm như trên, chúng ta chưa có cách nào theo dõi trạng thái của việc load script. Nhưng nếu chúng ta muốn sử dụng những hàm và biến được định nghĩa trong script, chúng ta cần sử dụng một phương thức khác. Truyền callback là một cách phổ thông nhất.

Bây giờ, muốn sử dụng những gì được định nghĩa trong script, chúng ta có thể cho vào callback:

Ý tưởng của việc này rất đơn giản, chúng ta truyền một hàm làm tham số của hàm khác, hàm này gọi là callback. Và hàm đó sẽ được gọi khi sau khi thực hiện xong một số đoạn code cần thiết. Đó cũng là phương thức xưa nay vẫn thường được sử dụng. Bất cứ một hàm nào hoạt động bất đồng bộ cũng cần cung cấp một tham số dành riêng cho việc truyền callback.

Callback lồng nhau

Việc sử dụng callback như trên rất tốt. Nhưng mọi việc sẽ phức tạp hơn khi chúng ta cần load nhiều hơn một script.

Với cách gọi callback lồng nhau như trên, sau khi script thứ nhất load xong, callback sẽ gọi việc load script thứ hai.

Code như trên vẫn còn trông rất đẹp, nhưng nếu chúng ta có nhiều script hơn nữa thì sao:

Việc sử dụng callback lồng nhau vẫn ổn nếu chúng ta lồng nhau ít cấp. Nhưng khi mức độ lồng nhau tăng lên, rõ ràng là không thể dùng cách này được. Mọi việc sẽ còn phức tạp hơn nữa khi các hoạt động bất đồng bộ này không phải lúc nào cũng thành công.

Xử lý khi gặp lỗi

Trong những ví dụ ở trên, chúng ta hoàn toàn không quan tâm đến trường hợp bị lỗi. Chúng ta nên nâng cấp code một chút để nó có thể xử lý thêm trường hợp này

Việc sử dụng rất đơn giản, hàm được truyền làm callback cần có hai tham số, tham số thứ nhất là lỗi (nếu không có lỗi thì truyền vào null) và tham số thứ hai là script được load.

Việc định nghĩa callback như trên là theo phong cách error-first callback. Convention rất đơn giản: tham số đầu tiên dùng để truyền lỗi khi nó xảy ra. Những tham số tiếp theo dùng để truyền kết quả cho trường hợp bình thường (khi đó, tham số đầu tiên sẽ là null). Bằng cách này, chúng ta chỉ cần định nghĩa một callback cho cả trường hợp có lỗi và không.

Callback hell

Những trường hợp ở trên, chúng ta đã xem xét cách sử dụng callback cho các hoạt động bất đồng bộ. Và trong trường hợp cần thiết, chúng ta cần phải sử dụng callback trong callback, thậm chí lồng nhau vài lớp. Nhưng càng lồng nhau nhiều, nguy cơ mất kiểm soát code sẽ càng tăng lên.

Vâng, phải nói là trông code rất đẹp. Mọi việc rất đơn giản theo flow như sau:

  • Load script1.js, nếu không có lỗi thì tiếp tục.
  • Load script2.js, nếu không có lỗi thì tiếp tục.
  • Load script3.js, nếu không có lỗi thì tiếp tục.
  • Load script4.js, nếu không có lỗi thì bắt đầu xử lý logic chúng ta cần.

Với cách làm như thế này thì code có thể tiếp tục mở rộng thêm nữa mà không gặp vấn đề gì. Nhưng khi mọi thứ trở nên phức tạp hơn, việc lồng nhau mức độ cao hơn, đặc biệt, khi chúng ta có những code với vòng lặp, các câu lệnh điều kiện, rẽ nhánh, v.v… việc kiểm soát code sẽ trở nên cực kỳ khó khăn.

Vấn đề này trong lập trình nói chung được gọi là pyramid of doom (do code trông như xây kim tự tháp). Riêng trong JavaScript nó còn được gọi với tên gọi là khác callback hell.

Nguyên nhân của callback hell là khi chúng ta cố gắng viết code JavaScript theo kiểu tuần tự như những ngôn ngữ khác. Nhưng vì đặc thù của hoạt động bất đồng bộ, nên việc tuần tự này không thể thực hiện được. Callback hell thường xảy ra ở những lập trình viên còn ít kinh nghiệm, tuy nhiên, kể cả người đã đi làm nhiều năm vẫn có thể gặp phải, bởi cấu trúc code lồng nhau thật quá phức tạp.

Ví dụ với code ở trên thì mọi thứ vẫn chạy tốt, nhưng chỉ cần đóng mở ngoặc sai một ly thôi là đi luôn một dặm. Trang web này có đưa ra một số phương án để phòng tránh callback hell cũng khá hay, có thể áp dụng được. Tuy nhiên, trong bài viết này, chúng ta sẽ tìm hiểu một phương án còn hay hơn nữa.

Một cách đơn giản để trông code có vẻ đơn giản hơn, tránh code trông như kim tự tháp kia là định nghĩa các hàm và gọi chúng như sau:

Bằng cách làm như trên, dù code không thay đổi về bản chất, nhưng kim tự tháp của chúng ta đã thấp đi đáng kể, bằng cách đó, callback hell sẽ khó xảy ra hơn. Mặc dù vậy, code này lại trở nên khó đọc hơn, để hiểu được hoạt động của nó, chúng ta phải do từ hàm này đến hàm khác. Nếu mức độ lồng nhau nhiều, thì việc làm này cũng tốn không ít thời gian.

May mắn cho chúng ta, từ khi ECMAScript 2015 (ES 6) ra đời, chúng ta đã có phương án tốt hơn rất nhiều để giải quyết.

Promise được giới thiệu kể từ ECMAScript 2015. Đây là một điểm sáng giúp chúng ta giải quyết các logic bất đồng bộ một cách tốt hơn.

Promise (lời hứa) có thể hiểu thế này: bạn hứa với mọi người sẽ làm việc XYZ và sẽ cho họ xem kết quả khi làm xong, nhưng bạn không biết chính xác khi nào thì sẽ xong. Họ cứ làm việc của họ trong lúc chờ đợi, khi công việc hoàn thành, bạn báo cho họ kết quả. Nếu chẳng may đại sự bất thành, bạn cũng thông báo cho họ không phải chờ nữa.

Như vậy, lời hứa được đảm bảo, ai nấy đều vui vẻ cả. Promise cũng được thiết kế với ý tưởng tương tự như vậy.

Một vài hoạt động bất đồng bộ, nó cần thời gian để hoàn thành, như ví dụ, đó là load một script khác. Rất nhiều code khác đang chờ công việc đó hoàn thành, promise l

0