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
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*
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 cũng chính là Iterators
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' }
Return
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' ]
yield*
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 }
Throw
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 cũng chính là Data Consumers
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
Generator Creation (1)
Ở stage này, chúng ta đang tạo ra generator g
.
Và execution sẽ dừng lại tại điểm A
.
Next đầu tiên (2)
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
.
Next tiếp theo (3)
Lúc này thì value đã đi qua yield
và như vậy execution sẽ bị ngừng lại.
Hãy dùng Cases
Implement Iterables
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 <= max; value += 2) { // When 'value' is even we want to 'yield' it // as our next value in the iteration. if (value % 2 === 0) yield value; } } // We can now user 'for-of' to iterate over the values. for (let e of evenNumbersUntil(10)) { console.log(e) // 0 // 2 // 4 // 6 // 8 // 10 }
Asynchronous Code
Ta còn có thể dùng generators với những async code như promises
.
Tiện thể thì cũng coi như là để giới thiệu về async/await
trên ES8.
Ví dụ dưới đây cũng sẽ cho ta thấy cách tìm kiếm một JSON file nhờ vào promises
. Đây là ví dụ của Jake Archibald thuộc developers.google.com.
function fetchStory() { get('story.json') .then(function (response) { return JSON.parse(response) }) .then(function (response) { console.log('Yey JSON!', response) }) }
Nhờ vào co library và một generator, code của chúng ta sẽ nhìn giống như synchronous code.
const fetchStory = co.wrap(function* () { try { const response = yield get('story.json') const text = yield JSON.parse(response) console.log('Yey JSON!', response) } })
Với async/await
thì nó vẫn sẽ khá giống so với phiên bản trên
async function fetchStory() { try { const response = await get('story.json') const text = await JSON.parse(response) console.log('Yey JSON!', response) } }
Lời Kết
Dưới đây là một map thể hiện mối quan hệ giữa generators và iterators, bởi Axel Rauschmayer trên Exploring ES6.
Generators chính là một cách thực hiện của iterable và nó dựa theo luật của iterable
của iterator
. Vì thế mà chúng có thể dùng để tạo ra iterables.
Tính năng tuyệt vời nhất của Generator là khiến execution bị hoãn lại. Với yield
nếu xài ES6.
Không những thế với yield*
, ta còn có thể gọi một generator nằm trong một generator khác.
Generators chính là cách thức để giúp việc phát triễn không đồng bộ trở thành đồng bộ.
Nguồn: Topdev via Medium