JavaScript Generator

Tuan Anh Le
andy.le
Published in
7 min readDec 30, 2018
Feature Image of Generator

Generator là một tính năng mới của ngôn ngữ JavaScript giới thiệu từ phiên bản ES6. Generator đóng vai trò cơ bản để xây dựng các phương thức xử lý bất đồng bộ khác (side effect), ví dụ: async/await, sagas.

Bài viết này giải thích khái niệm và cách thức hoạt động của Generator. Dựa trên cơ sở đó, giới thiệu một số thư viện phổ biến được xây dựng dựa trên Generator.

Iterator

Theo định nghĩa, Iterators là đối tượng định nghĩa cách thức truy cập tuần tự vào từng phần tử (item) trong một dữ liệu dạng tập hợp (collection). Iterator ghi nhớ chỉ mục (index) của phần tử đang được truy cập.

Xây dựng một iterator

Ví dụ trên tạo ra một iterator để truy cập các phần tử trong một mảng string. Theo định nghĩa của iterator protocol, mỗi lời gọi method: it.next() sẽ duyệt qua một phần tử trong tập hợp và trả về một object với hai thuộc tính:

  • value: giá trị trả về của phần tử mà iterator đang duyệt qua
  • done (boolean): trạng thái cho biết iterator đã duyệt qua hết tất cả các phần tử trong mảng.

Một ví dụ khác của iterator là Array iterator được sử dụng thông dụng trong JavaScript. Array iterator trả về từng phần tử trong Array. Iterator dễ bị nhầm lẫn khi so sánh với Array:

  • Array có giới hạn về size (memory được allocated khi khai báo)
  • Iterator không bị giới hạn về size

Hãy xem xét một ví dụ về iterator:

Function makeRangeIterator xây dựng một iterator trả về các giá trị 0, step, 2 * step, 3 * step, etc…Iterator được tạo ra không bị giới hạn bởi số lượng phần tử trả về cũng như giá trị cận trên (end =Infinity).

Một điểm giới hạn trong việc xây dựng Iterator là cấu trúc phức tạp, yêu cầu người phát triển phải quản lý chặt chẽ trạng thái nội tại (internal state) của iterator.

Theo ví dụ trên, function makeRangeIterator cần quản lý đồng thời biến state: iterationCount (tracking số lượng iteration thực thi), cũng như việc trả về các phần tử truy cập. Do đó, Generator được giới thiệu nhằm tách biệt hai vai trò trên, giúp cho việc lập trình dễ dàng hơn.

Generator

Generator là các function trả về các iterator trong quá trình thực thi (a factory for Iterator). Generator bao gồm hai thành phần cơ bản:

  • Generator Function
  • Generator Iterator

Có thể giải thích về hai thành phần này qua ví dụ cơ bản dưới đây:

Generator function sample bao gồm ký tự (*) được đặt sau từ khoá function để phân biệt giữa function thông thường và generator function. Bên trong Generator function, chúng ta có thể sử dụng yield [expression]để pause việc xử lý hiện tại của generator và trả về một đối tượng IteratorResult với hai thuộc tính:

  • value: kết quả của việc thực thi biểu thức [expression] sau lệnh yield.
  • done (boolean): mang giá trị false khi generator chưa kết thúc.

Trong dòng lệnh tiếp theo, var it = sample() trả về Generator Iterator. Iterator này có khả năng resume lại Generator function thông qua việc gọi phương thức iterator.next(param).

Trong phương thức next(), chúng ta có thể chú ý kĩ hơn đến tham số param. Có hai trường hợp:

  • it.next(): giá trị trả về của lời gọi yield được xem như input cho lần gọi yield kế tiếp
  • it.next(value): giá trị value được đưa vào, thay đổi internal state của Generator function. Cụ thể hơn, value sẽ được coi như input cho lần gọi yield tiếp theo.

Ví dụ:

Theo ví dụ trên, Generator function, farBeer bao gồm duy nhất một lời gọi yield để in ra một chuỗi kí tự. Giá trị trả về reply được so sánh với “ipa” để in ra kết quả tương ứng (“No soup for you” or “Ok, soup”).

Trong block đầu tiên:

it.next(“large”) được gọi kèm theo input “large”. Biến reply khi này sẽ được gán giá trị “large” và so sánh với “ipa”. Do hai xâu này khác nhau nên kết quả in ra message: “No soup for you”.

Trong block tiếp theo:

it.next(“ipa”) được gọi kèm theo input “ipa”. Biến reply khi này mang giá trị “ipa” và do đó kết quả sẽ in ra : “Ok, soup for you” ;).

Ví dụ trên cho thấy hình thức giao tiếp hai chiều giữa generator và iterator.

Generator thực thi và trả về (yield) giá trị cho iterator. Iterator có thể thay đổi internal state của Generator thông qua giá trị tham số của method next

Xử lý bất đồng bộ với Generator Function

Như đã đề cập, tại một thời điểm, câu lệnh yield có khả năng pause quá trình thực thi của một generator function. Do đó, nó cho phép gửi đi các yêu cầu bất đồng bộ (asynchronous request), và chờ đợi kết quả trả về trước khi tiếp tục resume lại quá trình thực thi trước đó.

Để làm rõ hơn quá trình này, hãy xem xét ví dụ sau: một ứng dụng sử dụng hàm init để query các thông tin users / feeds từ phía backend. Client cần gửi đi một XHR request (XMLHttpRequest) và chờ đợi kết quả trả về.

Trong trường hợp sử dụng Promise, function init sẽ xây dựng như sau:

Với cách làm trên, việc xử lý nhiều yêu cầu bất đồng bộ phụ thuộc nhau về kết quả trả về có thể làm cho đoạn code của chương trình trở nên phức tạp (callback hell) và khó kiểm soát.

Vì vậy, người ta cần tìm các phương thức khác nhau giúp cho lập trình viên có thể viết các đoạn code xử lý async request dưới một hình thức giống như “synchronous”. (write non-blocking code in a nice-ish way)

Với trường hợp của Generator function, chúng ta có thể sử dụng yield để chờ đợi kết quả trả về từ async function: getUsersFromDb:

Theo cách thức này, khi hàm yield được gọi, generator sẽ pause lại quá trình thực thi và trả quyền kiểm soát về cho caller (trong trường hợp này là Iterator). Phía iterator nhận được kết quả trả về là một JavaScript object:

{ value: Promise {}, done: boolean }

Iterator cần chờ đợi Promise hoàn thành (resolved), resume và gửi trả lại kết quả cho phía Generator function. Hãy xem xét ví dụ sau:

Trong đoạn code trên, phía caller (external code) sử dụng const iteration để xử lý kết quả trả về khi Promise được hoàn thành. Trên thực tế, caller thường sử dụng các thư viện (ví dụ, Redux-saga) giúp cho việc xử lý với nhiều Promise đơn giản hơn. Các thư viện này cung cấp function, có khả năng nhận Generator dưới dạng tham số, thực thi Generator và xử lý kết quả Promise trả về từ yield

Async/Await

Async/Await được giới thiệu từ 2015 trong ES2017. Dựa trên Generator function, cấu trúc này giúp developer xử lý các yêu cầu bất đồng bộ đơn giản hơn.

Chúng ta có thể viết lại ví dụ trong phần trước dựa trên async/await một cách đơn giản hơn:

Từ khoá await đặt trước các câu lệnh trả về đối tượng Promise, và chỉ được sử dụng trong function được đánh dấu bởi từ khoá async. Trong quá trình thực thi, function test() sẽ pause lại khi gọi đến request: getUsersFromDb(), chờ đợi kết quả trả về khi Promise hoàn thành, và tự động gán kết quả trả về cho users

Với async/await, chúng ta có thể viết các đoạn xử lý async request mà không cần sử dụng phương thức .then(), callback(), callback hell. Tuy nhiên, vẫn cần một số lưu ý trong quá trình sử dụng phương thức này:

  1. Các async function luôn trả về một promise: function cần paused quá trình thực thi mỗi khi xử lý câu lệnh bắt đầu với await [Promise] cho đến khi Promise được resolve. Vì vậy chính các function này cũng là một async block cho đến khi kết quả cuối cùng được resolve.
  2. Bên trong async function, các await [Promise] chỉ được xử lý tuần tự (không đồng thời). Từng promise phải được resolve trước khi tiếp tục xử lý đến await tiếp theo.

Kết luận

Generator là một trong những phương thức hiệu quả để làm việc với dữ liệu collection, đồng thời là cơ sở để xử lý các yêu cầu bất đồng bộ. Trái ngược với mô hình observer/subscriber, Generator sinh ra các giá trị theo hình thức thụ động (passive procedurer) mỗi khi có yêu cầu từ Iterator (active consumer).

Bằng việc sử dụng Generator, lập trình viên có thể phát triển các đoạn mã xử lý yêu cầu bất động bộ một cách đơn giản và rõ ràng hơn. Các câu lệnh yield trong Generator có thể trả về các Promise từ những yêu cầu bất đồng bộ, tuy nhiên điều đó đòi hỏi phía caller cần chú ý việc xử lý kết quả resolve của Promise.

Cấu trúc async/await dựa trên Generator để đơn giản hoá cách xử lý kết quả resolve trả về từ mỗi Promise. Tuy vậy cấu trúc này không thể xử lý đồng thời với nhiều Promise trong cùng một thời điểm. Đó cũng là lý do một số thư viện như Redux-saga vẫn cần dựa trên Generator để cung cấp các tính năng nâng cao trong việc quản lý các xử lý bất đồng bộ.

Redux-saga uses an ES6 feature called Generators to make those asynchronous flows easy to read, write and test… By doing so, these asynchronous flows look like your standard synchronous JavaScript code. (kind of like async/await, but generators have a few more awesome features we need)

--

--