Cùng khám phá sức mạnh của 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

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 yieldvà 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