Function generator là gì

June 13, 2021

3 min read

Mở đầu

Một trong những tính năng thú vị nhất dành cho những nhà phát triển được giới thiệu cùng Javascripts ES6, đó là một loại function rất đặc biệt, gọi là Generator.

Trong javascript một khi function được thực thi thì nó sẽ được đảm bảo run-to-completion tức là những phần code khác không thể can thiệp, làm gián đoạn quá trình chạy của function đó. Tuy nhiên Generator không hành xử theo lẽ thông thường như thế, chút nữa chúng ta sẽ cùng phân tích kĩ hơn. Cho dù bạn có nhận ra hay không, chúng ta có thể luôn thừa nhận một điều khá cơ bản rằng : Khi một function bắt đầu chạy, nó sẽ chạy cho đến khi hoàn thành trước khi bất kì đoạn code khác nào được chạy tiếp theo.

Ví dụ:

Tại đây, vòng lặp for tốn khá nhiều thời gian để hoàn thành, nhưng hàm callback với câu lệnh console.log[..] không thể ngắt quãng function foo[] khi mà nó đang chạy, vì thế nó bị kẹt ở cuối hàng đợt, và kiên nhẫn đợi cho đến lượt mình.

Điều gì sẽ xảy ra nếu foo [] có thể bị gián đoạn? Liệu chăng điều đó sẽ phá hủy chương trình của chúng ta ?

Chạy..Dừng..Chạy tiếp

Với Generator, chúng ta có một loại function khác biệt, có thể dừng lại giữa chừng, một hay nhiều lần, và được khôi phục lại sau đó, cho phép các đoạn code khác chạy trong thời gian bị tạm dừng này. Đặc biệt, không có gì có thể tạm dừng một Generator từ bên ngoài; nó tạm dừng chính nó khi đi qua một yield. Tuy nhiên, khi một Generator được dừng lại, nó sẽ không thể tự khôi phục lại. Cần phải có một điều khiển từ bên ngoài để khởi động lại nó.

Vì thế, cơ bản thì một hàm Generator có thể tạm dừng lại và được khởi động lại, bao nhiêu lần tùy ý. Trong thực tế, bạn có thể chỉ định một hàm tạo ra một vòng lặp vô hạn [như là while [true] {..}] chạy suốt không bao giờ kết thúc. Trong khi thông thường nó có vẻ điên rồ hay là một sai lầm ở một chương trình JS bình thường, với Generator lại hoàn toàn bình thường và đôi khi thực hiện chính xác những gì bạn muốn làm!

Quan trọng hơn nữa, việc dừng và khởi động này không chỉ là kiểm soát việc thực hiện của function, mà nó còn cho phép giao tiếp thông tin 2 chiều giữa trong và ngoài hàm Generator. Với các hàm bình thường, bạn sẽ có được các tham số ở đầu và một giá trị trả về ở cuối. Với Generator, bạn gửi tin nhắn ra với mỗi lần yield, và bạn nhận được phản hồi cho mỗi lần trở lại.

Cú pháp

Lưu ý, dấu * dùng để báo hiệu đây là kiểu hàm Generator. yield__ được coi như một “biểu thức yield” [không phải câu lệnh] bỏi vì khi khởi động lại Generator, chúng ta sẽ gửi dữ liệu vào bên trong. Và bất cứ dữ liệu gì ta gửi sẽ là kết quả của biểu thức yield__

Ví dụ:

Biểu thức yield "foo" sẽ gửi chuỗi “foo” ra ngoài và tạm dừng hàm generator tại thời điểm đó. Khi được khởi động lại, bất kì giá trị nào được gửi vào sẽ là kết quả của biểu thức, sau đó được cộng thêm 1, và gán vào biến x.

Chắc bạn có thể dễ dàng nhận ra sự giao tiếp 2 chiều ở đây. Bạn gửi giá trị “foo” ra ngoài, tạm dừng lại. Tại 1 thời điểm sau đó [có thể ngay lập tức, cũng có thể là 1 khoảng thời gian dài]. Generator sẽ được khởi động lại và nhận về dữ liệu. Có thể coi từ khóa yield như một kiểu request cho một giá trị.

Tổng kết

Ok, đó là một số khái niệm cơ bản về Generator. Bạn có thể tự hỏi rằng, những thứ này có thể giúp ích gì cho dự án của mình. Và tại đây thì cuộc vui mới thực sự bắt đầu !! Một số liên quan đến:

  1. Làm sao để kiểm soát và xử lý lỗi ?
  2. Một hàm Generator có thể gọi một hàm Generator khác không ?
  3. Làm thế nào để làm việc với các tác vụ không đồng bộ ?

Những câu hỏi đó, và nhiều hơn nữa, sẽ được khám phá trong bài viết tiếp theo.. Còn giờ xin được tạm biệt !!

Nguồn : //davidwalsh.name/es6-generators

Generators có thể xem như là cách áp dụng của iterables

Điều khiến generators trở nên đặc biệt bởi vì chúng là những functions có khả năng hoãn lại quá trình execution mà vẫn giữ nguyên được context.

Đây là một tính năng rất quan trọng khi ta phải dùng tới những executions đòi hỏi phải có quãng pause nhưng context phải được để nguyên nhằm để recover lại trong tương lai khi cần đến.  

Bạn có từng nghe qua quá trình phát triển async chưa?

Syntax [Cú pháp] cho generators bắt đầu với function* declaration của chính nó [nhớ lưu ý cái asterisk] và  yield dành cho khi generator muốn dừng [pause] execution.

function* generator[] { // A yield 'foo' // B }

Với  next function, chúng ta có thể kiểm soát được quá trình tạo ra một generator từ  generator sẵn có.

Khi chạy  next function, thì code của  generator sẽ được thực hiện [execute] và cho đến khi gặp  yield thì sẽ ngừng lại.

Lúc đó,  yield sẽ xen vào và khiến cho  generator execution bị đình chỉ [pause].

const g = generator[] g.next[] // { value: 'foo', done: false } // Our generator's code A gets executed // and our value 'foo' gets emitted through yield. // After this, our generator's execution gets suspended. g.next[] // { value: undefined, done: true } // At this stage the remaining code [i.e. B] gets executed. // Because no value is emitted we get 'undefined' as the value, // and the iterator returns 'true' for iteration done.

yield được sinh ra cùng lúc với generator và cho phép chúng ta đưa ra các giá trị mà mình muốn. Tuy nhiên, nó chỉ thực hiện được khi ở trong phạm vi của generator.

Nếu thử dùng yield  với một giá trị trong callback thì cho dù đã declared trong generator thì nó vẫn sẽ bị lỗi.

function* generator[] { ['foo','bar'].forEach[e => yield e] // SyntaxError // We can't use 'yield' inside a non-generator function. }

yield* được tạo ra nhằm có khả năng gọi một generator nằm trong một generator khác.

function* foo[] { yield 'foo' } // How would we call 'foo' generator inside the 'bar' generator? function* bar[] { yield 'bar' foo[] yield 'bar again' } const b = bar[]; b.next[] // { value: 'bar', done: false } b.next[] // { value: 'bar again', done: false } b.next[] // { value: undefined, done: true }

Bạn có thể thấy b iterator, thuộc  bar  generator, không hề chạy như đúng ý ta khi call foo.

Đó là mặc dù foo  execution cho ra một iterator, nhưng ta sẽ không có lặp lại [iterate] nó được.

Vì thế mà ES6 cần có operator  yield*

function* bar[] { yield 'bar' yield* foo[] yield 'bar again' } const b = bar[]; b.next[] // { value: 'bar', done: false } b.next[] // { value: 'foo', done: false } b.next[] // { value: 'bar again', done: false } b.next[] // { value: undefined, done: true }

Đồng thời nó cũng hoàn toàn có thể áp dụng với data consumers

for [let e of bar[]] { console.log[e] // bar // foo // bar again } console.log[[...bar[]]] // [ 'bar', 'foo', 'bar again' ]

 yield* có khả năng kiểm tra và chạy qua hết tất cả ngõ ngách trong generator để yield ra phần nó cần.

function* bar[] { yield 'bar' for [let e of foo[]] { yield e } yield 'bar again' }

Generators thực chất là những iterables đơn giản. Nói cách khác chúng cũng sẽ theo luật của iterable và  iterator .

Luật của iterable cho ta biết một object sẽ nên return một function itera với key là  Symbol.iterator.

const g = generator[] typeof g[Symbol.iterator] // function

Còn luật của iterator cho ta biết iterator nên là một object chỉ tới yếu tố tiếp theo của iteration. Object này phải chứa một function gọi là next

const iterator = g[Symbol.iterator][] typeof iterator.next // function

Bởi vì generators là iterables nên chúng ta có thể dùng data consumer  for-of, để iterate [lặp lại] giá trị của generators [values].

for [let e of iterator] { console.log[e] // 'foo' }

Chúng ta còn có thể add vào return cho generator, thế nhưng return sẽ hoạt động hơi khác đi tùy thuộc vào cách generators’ data được iterated.

function* generatorWithReturn[] { yield 'foo' yield 'bar' return 'done' } var g = generatorWithReturn[] g.next[] // { value: 'foo', done: false } g.next[] // { value: 'bar', done: false } g.next[] // { value: 'done', done: true } g.next[] // { value: undefined, done: true }

Khi ta thực hiện iteration bằng tay, sử dụng  next, sẽ nhận được returned value [i.e. done ] cũng chính là value cuối của iterator object và khi done  đưa ra kết quả true.

Mặt khác, khi sử dụng defined data consume như for-of hoặc destructuring thì returned value sẽ bị bỏ qua.

for [let e of g] { console.log[e] // 'foo' // 'bar' } console.log[[...g]] // [ 'foo', 'bar' ]

Như bạn đã biết  yield* được tạo ra nhằm có khả năng gọi một generator nằm trong một generator khác.

Ngoài ra, nó còn cho phép chúng ta lưu trữ value returned bằng executed generator.

function* foo[] { yield 'foo' return 'foo done' } function* bar[] { yield 'bar' const result = yield* foo[] yield result } for [let e of bar[]] { console.log[e] // bar // foo // foo done }

Chúng ta có thể dùng throw trong một generator và next  sẽ truyền exception ra.

Và khi một exception bị đẩy ra, iterator [lặp] sẽ bị phá và state của nó sẽ được set thành  done: true

function* generatorWithThrow[] { yield 'foo' throw new Error['Ups!'] yield 'bar' } var g = generatorWithReturn[] g.next[] // { value: 'foo', done: false } g.next[] // Error: Ups! g.next[] // { value: undefined, done: true }

Generators ngoài khả năng như một data producers, với yield, nó cũng có thể consume data khi dùng next.

function* generatorDataConsumer[] { // A console.log['Ready to consume!'] while [true] { const input = yield; // B console.log[`Got: ${input}`] } }

Có một vài điểm khá thú vị sau đây

// [1] var g = generatorDataConsumer[] // [2] g.next[] // { value: undefined, done: false } // Ready to consume! // [3] g.next['foo'] // { value: undefined, done: false } // Got: foo

Ở stage này, chúng ta đang tạo ra generator g.

Và execution sẽ dừng lại tại điểm A.

Execution đầu tiên của next giúp cho generator được executed cho tới khi gặp phải yield.

Tất cả các giá trị [value] trong stage này khi đi qua  next sẽ bị lơ đi. Nguyên nhân là vì vẫn chưa có gặp một  yield nào cả.

Và execution của chúng ta chỉ dừng lại tại điểm B  khi một  value nào đó được đưa ra bởi  yield.

Lúc này thì value đã đi qua yieldvà như vậy execution sẽ bị ngừng lại.

Bởi generators là một iterable implementation, khi chúng được tạo ra thì chúng ta cũng sẽ có một iterable object với từng yield đại diện cho một giá trị sẽ được đưa ra trên từng iteration. Nói cách khác chúng ta có thể dùng generators để tạo ra iterables.

Ví dụ sau đây sẽ thể hiện generator như là iterable  với khả năng lập một dãi các số nguyên cho tới khi nó đạt  max. Và ta cũng dùng for-of  để lập những giá trị trên.

Các bạn cũng cần lưu ý rằng yield sẽ khiến các execution bị dừng lại tại một điểm và các iteration sẽ khiến cho execution chạy tiếp tại các điểm đó.

function* evenNumbersUntil[max] { for [let value = 0; value

Chủ Đề