Skip to main content

Generator (Generator)_ES6 Notes 2

Free2016-04-03#JS#js生成器#生成器用法#JavaScript生成器

According to the firsthand account from the implementer of this feature in Firefox, supplementing some details and application scenarios

Preface

Actually, previously summarized in [Ayqy: JavaScript Generator](/articles/javascript 生成器/), this is not intentionally repeating. Previously was summary based on MDN documentation, focusing on syntax rules, this article according to firsthand account from implementer of this feature in Firefox, supplements some details and application scenarios

To avoid repetition, this article no longer explains syntax rules (function* + yield), for syntax details please see previous article

1. Function and Internal Principle

generator (generator) is used to create iterators, syntax is very concise (function* + yield)

When generator executes yield statement, generator's stack structure (local variables, parameters, temporary values, generator's internal current execution position) is removed from stack. But generator object retains reference to this stack structure (backup), so later calling .next() can reactivate stack structure and continue execution

For example:

// Define generator
var gen = function*() {
    console.log('before yield 1');
    yield 1;
    console.log('before yield 2');
    yield 2;
}
// Call generator returns iterator
var iter = gen();
iter.next();    // before yield 1
                // Object {value: 1, done: false}
iter.next();    // before yield 2
                // Object {value: 2, done: false}
iter.next();    // Object {value: undefined, done: true}
iter.next();    // Object {value: undefined, done: true}

yield statement divides function body into several segments, .next() executes one segment at a time

2. Iterators and Generators

What function* defines is called iterator's generator (abbreviated as generator), because calling it returns an iterator

All generators have built-in implementations of .next() and [Symbol.iterator]() methods, we only need to write loop part's behavior. Function body after function* is like loop body of loop structure, for example:

function* gen(arr) {
    for (var i = 0; i < arr.length; i++) {
        yield arr[i];
    }
}
var iter = gen([1, 2, 4]);
console.log(iter.next());   // Object {value: 1, done: false}
console.log(iter.next());   // Object {value: 2, done: false}

Among them gen's function is to turn continuous array into "breathing" array, for loop originally can't stop, but yield indeed made it stop, this is also one of generator's major features. Using this feature can implement many interesting things, such as using animation to display quicksort process, pseudo code as follows:

function quicksort(arr) {
    // sort
    forloop {
        updateSortedArr();  // Complete one pass of sorting
        displaySortedArr(); // Display this pass's sorting result
    }

    return sortedArr;
}

Of course, this way cannot see animation. Because n passes of sorting complete instantly (too fast, can't see anything clearly where's the animation, didn't change at all). Easy to think of workaround: first store each pass's result, finally display:

function quicksort(arr) {
    var tmpArr = [];
    forloop {
        updateSortedArr();      // Complete one pass of sorting
        tmpArr.push(sortedArr); // Store it
    }
    // Animation display sorting process
    anim(tmpArr);
}

After getting each pass's result, can display however want. Feels not very difficult, okay, if want to animate display movement of 2 pointers in each pass of quicksort? We seem to need to record pointer movement's tmpArr again, if this array is very large, if... This will inevitably need more memory space to record already happened things

Think carefully, during sorting process, data needed for animation display already exists, so can we sort while animating display? Of course can:

function* quicksort(arr) {
    forloop {
        yield sortedArr;
        // Or
        // yield step;
    }
}
var iter = quicksort(arr);
// Animation display sorting process
function anim() {
    display(iter.next());
    setTimeout(anim, 300);
}
anim();

Yes, generator makes loop able to "breathe", create animation during breathing process

P.S. Actually even without generator, we can also make loop "stop", please see [Ayqy: JavaScript Implement yield](/articles/javascript 实现 yield/)

P.S. For specific details of quicksort, please see Ayqy: Sorting Algorithm Quicksort Analysis

3. Characteristics

Generator's characteristics are as follows:

  • Ordinary functions cannot self-pause, generator functions can

  • yield only valid in direct scope of function*, yield in anonymous function inside function* is illegal

  • Can handle infinite sequences. Cannot construct infinitely large arrays, but can use generator to implement construction rules of infinite sequences, to handle infinite sequences

  • Provides another idea for returning arrays, return generator instead of array, trade time for space

  • Can refactor complex loops, split it into 2 parts, convert data generation part to generator, then for...of iterate through these data

  • Can quickly create iterators, make any object iterable, specifically please see [Ayqy: for…of Loop_ES6 Notes 1](/articles/for-of 循环-es6 笔记 1/)

  • Easy to extend iterators, very easy to implement filtering etc. operations

Point 2 needs attention, because most materials introducing generators won't mention this point, but we indeed cannot setTimeout delay yield in generator. Using generator to extend iterator is a good choice, code is very natural, example as follows:

// Extend iterator
function* filter(isValid, iterable) {
    for (var val of iterable) {
        if (isValid(val)) {
            yield val;
        }
    }
}
// test
function isValid(val) {
    return val > 1;
}
for (var val of filter(isValid, [0, 1, 2, 4])) {
    console.log(val);
}

Wrapping iterator with generator, has a kind of seamless connection beauty, doesn't it?

4. Advanced Techniques

1. Affect Generator's Logic Flow from Outside

Iterator's next(returnVal) method accepts optional parameter, parameter will be used as return value of previous yield statement in generator, this way caller can affect generator's logic flow from outside, for example:

function* gen() {
    var water = yield 'give me a cup of pure water';
    yield water.drink();
}
// test
var iter = gen();
console.log(iter.next());
console.log(iter.next({
    name: 'pure water',
    drink: function() {
        return 'hmm, well';
    }
}));

First yield asks caller for a cup of water, second .next() hands water to generator, then second yield drinks the water

This is a two-way interaction process, in practical applications, judge what generator needs according to next()'s return value, then pass it through next .next(), isolate complex logic in generator, calling is as easy as conversation

2. Terminate Iterator

Note, it's iterator, there are two methods to terminate an iterator:

  • Iterator's throw(err) method, effect is like yield expression in generator calls a function and throws error

  • Iterator's return(returnVal) method, accepts optional parameter, parameter will be returned as value (done is true), generator only executes finally code block and no longer resumes execution

throw example as follows:

// throw
function* gen() {
    try {
        yield 1;
        yield 2;
    } catch (err) {
        console.log('error occurs: ' + err);
    } finally {
        console.log('clean up');
    }
}
var iter = gen();
console.log(iter.next());   // Object {value: 1, done: false}
console.log(iter.throw(new Error('err')));  // error occurs: Error: err
                                            // clean up
                                            // Object { value: undefined, done: true }
console.log(iter.next());   // Object {value: undefined, done: true}

throw(err) indicates iterator abnormally closes, used to notify generator internal to execute cleanup work in finally

While return() indicates iterator normally closes, example as follows:

console.log(iter.next());   // Object {value: 1, done: false}
console.log(iter.return('ok')); // clean up
                                // Object { value: "ok", done: true }
console.log(iter.next());   // Object {value: undefined, done: true}

Note: 'ok' as value returns immediately, not returned at next .next()

P.S. Chrome49 still doesn't support return(), FF throw() after still can return(), but if first return() then throw() will error

3. Splice Iterators

yield* iter can splice iterators, supports calling another generator in one generator, for example:

var gen1 = function* (){
    yield 1;
    yield 2;
}
var gen2 = function* (){
    yield* gen1();
    yield 3;
    yield 4;
}
for (var val of gen2()) {
    console.log(val);   // 1 2 3 4
}

5. Summary

Generator can make execution flow "breathe", can make things that can't stop pause, can be used to refactor loops, can control infinite sequences, can wrap iterators... many benefits

References

  • "ES6 in Depth": Free e-book provided by InfoQ Chinese site

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment