To Yield or Not To Yield - A layman's guide to ES6 Generator Functions
Bài đăng này đã không được cập nhật trong 3 năm
Generators
One of the most exciting and weired new features of ES2015 are the Generators. How weired you ask? Kyle Simpson, author of the You don't know JS
series wrote The name Generator is a little strange, but the behavior may seem a lot stranger
on his article. So yeah, 'pretty darn weired', I'd say.
To put it in layman's terms, Generators are a special breed of fuctions, which can pause its execution by itself for indefinite time, and after that return to the paused part and reexecute.
To directly quote MDN, Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.
Calling a generator function does not execute its body immediately; an iterator
object for the function is returned instead. When the iterator's next()
method is called, the generator function's body is executed until the first yield
expression, which specifies the value to be returned from the iterator or, with yield*
, delegates to another generator function. The next()
method returns an object with a value property containing the yielded value and a done property which indicates whether the generator has yielded its last value as a boolean. Calling the next()
method with an argument will resume the generator function execution, replacing the yield
statement where execution was paused with the argument from next()
.
Availability
At the time of writing this article, node does not directly support Generator functions.
node --v8-options | grep harmony
--es_staging (enable test-worthy harmony features (for internal use only))
--harmony (enable all completed harmony features)
--harmony_shipping (enable all shipped harmony features)
--harmony_default_parameters (nop flag for "harmony default parameters")
--harmony_destructuring_assignment (nop flag for "harmony destructuring assignment")
--harmony_destructuring_bind (nop flag for "harmony destructuring bind")
--harmony_regexps (nop flag for "harmony regular expression extensions")
--harmony_proxies (nop flag for "harmony proxies")
--harmony_reflect (nop flag for "harmony Reflect API")
--harmony_tostring (nop flag for "harmony toString")
--harmony_array_prototype_values (enable "harmony Array.prototype.values" (in progress))
--harmony_object_observe (enable "harmony Object.observe" (in progress))
--harmony_function_sent (enable "harmony function.sent" (in progress))
--harmony_sharedarraybuffer (enable "harmony sharedarraybuffer" (in progress))
--harmony_simd (enable "harmony simd" (in progress))
--harmony_do_expressions (enable "harmony do-expressions" (in progress))
--harmony_regexp_property (enable "harmony unicode regexp property classes" (in progress))
--harmony_string_padding (enable "harmony String-padding methods" (in progress))
--harmony_regexp_lookbehind (enable "harmony regexp lookbehind")
--harmony_tailcalls (enable "harmony tail calls")
--harmony_object_values_entries (enable "harmony Object.values / Object.entries")
--harmony_object_own_property_descriptors (enable "harmony Object.getOwnPropertyDescriptors()")
--harmony_exponentiation_operator (enable "harmony exponentiation operator `**`")
--harmony_function_name (enable "harmony Function name inference")
--harmony_instanceof (enable "harmony instanceof support")
--harmony_iterator_close (enable "harmony iterator finalization")
--harmony_unicode_regexps (enable "harmony unicode regexps")
--harmony_regexp_exec (enable "harmony RegExp exec override behavior")
--harmony_sloppy (enable "harmony features in sloppy mode")
--harmony_sloppy_let (enable "harmony let in sloppy mode")
--harmony_sloppy_function (enable "harmony sloppy function block scoping")
--harmony_regexp_subclass (enable "harmony regexp subclassing")
--harmony_restrictive_declarations (enable "harmony limitations on sloppy mode function declarations")
--harmony_species (enable "harmony Symbol.species")
--harmony_instanceof_opt (optimize ES6 instanceof support)
So, we will need to use Babel to transpile our code. For the sake of this article,
we will use REPL.it for practicing the examples. Beside every example, you will find a link for the source of that example, and a second link to REPL.it
with the code, where you can tinker & tweak with it.
A Quick Example
Hello Yield
Lets consider this example from MDN. You can also try the live demo here.
function* idMaker() {
let index = 0;
while (index < 3)
yield index++;
}
Here we declare a generator function who has an initial index of 0, and will keep sending us data until index is 3.
To invoke the it, lets assign the generator function to a variable, and call .next()
in it.
const gen = idMaker();
gen.next() // { value: 0, done: false }
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: undefined, done: true }
Now when, yeild
is faced, the code halts the execution, and returns a Generator Object to the caller. The structure is
{
value: "What was passed with the yield statement, in our case 'index++'",
done: "true/false"
}
For the 1st three gen.next()
we get the result as expected. For the fourth gen.next()
(while is false, in our code), we get the result
{ value: undefined, done: true }
Here, done: true
confirms that the generator has ran it's course completely.
Ways to create a Generator
- Via a generator function declaration
function* genFunc() { ··· }
let genObj = genFunc();
- Via a generator function expression:
const genFunc = function* () { ··· };
let genObj = genFunc();
- Via a generator method definition in an object literal:
let obj = {
* generatorMethod() {
···
}
};
let genObj = obj.generatorMethod();
- Via a generator method definition in a class definition (which can be a class declaration or a class expression
class MyClass {
* generatorMethod() {
···
}
}
let myInst = new MyClass();
let genObj = myInst.generatorMethod();
Ways to halt/terminate a Generator
-
A
yield
, which causes the generator to once again pause and return the generator's new value. The next time next() is called, execution resumes with the statement immediately after the yield. -
throw
is used to throw an exception from the generator. This halts execution of the generator entirely, and execution resumes in the caller as is normally the case when an exception is thrown. -
The end of the generator function is reached; in this case, execution of the generator ends and an IteratorResult is returned to the caller in which the value is undefined and done is true.
-
A
return
statement is reached. In this case, execution of the generator ends and an IteratorResult is returned to the caller in which the value is the value specified by the return statement and done is true.
Yield
In plain Englsih Yield
means to pause
or to surrender
or laying down arms
. That's why in ye old days knights and sword fighters would shout 'I YIELD! I YIELD!!' whenever in a bad position during a sword fight, and try to find an opportunity to throw sand at the opponents eyes.
An ES6 Generator Function
Now, there were these regular knights, but also, there was A Black Knight who never yields.
Normal JS functions are like the Black Knight, never yeilding to anyone/anything, once initiated, it won't stop until it executes completely.
Regular JS Function
Take this code by Kyle Simpson
as an example
setTimeout(function(){
console.log("I am Late Latif from Comilla");
}, 1); // only 1ms delay, human brain cannot even register differences less than 100ms
function foo() {
for (var i=0; i<=1E10; i++) {
console.log(i);
}
}
foo();
The for loop
will take very long to complete. When the loop is being executed, the callback
function with 1ms delay cannot execute, and have to wait for the loop to finish. Finally, we get the output
1
2
..
..
1E10
I am Late Latif from Comilla
The yield
keyword is used to halt the generator function execution. The value/expression following the yield
is also returned as the value
attribute of generator objects.
Some more examples
Calling another Generator Function inside a Generator
To call another generator function from inside of a generator function, we need to use the yield* functionName
syntax.
Lets consider our 2nd example from MDN
function* generatorB(i) {
yield i+1;
yield i+2;
yield i+3;
}
function* generatorA(i) {
yield i;
yield* generatorB(i);
yield i+10;
}
We have two generator functions here, generatorA
takes an input i
, yields i
immediately, after that calls another generator function generatorB
with parameter i
, which on it's lifespan halt for 3 times i + [1,2,3]
, and finally after generatorB's
execution ends
var gen = generator(10);
console.log(gen.next().value); // inside generatorA
// 10
console.log(gen.next().value); // inside generatorB
// 11
console.log(gen.next().value); // inside generatorB
// 12
console.log(gen.next().value); // inside generatorB
// 13
console.log(gen.next().value); // inside generatorA again
// 20
Uses
Implementing iterators via generators
The following function returns an iterable over the properties of an object, one [key,value] pair per property
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let josim = { first: 'Josim', last: 'Vai' };
for (let [key,value] of objectEntries(josim)) {
console.log(`${key}: ${value}`);
}
// output
// first: Josim
// last: Vai
Making asynchronous codes synchronous
In the following code, we will use the control flow library co to asynchronously retrieve two JSON files. Note how, in line (A), execution blocks (waits) until the result of Promise.all() is ready. That means that the code looks synchronous while performing asynchronous operations.
co(function* () {
try {
let [croftStr, bondStr] = yield Promise.all([ // (A)
getFile('http://localhost:8000/croft.json'),
getFile('http://localhost:8000/bond.json'),
]);
let croftJson = JSON.parse(croftStr);
let bondJson = JSON.parse(bondStr);
console.log(croftJson);
console.log(bondJson);
} catch (e) {
console.log('Failure to read: ' + e);
}
});
Conclusion
This was a fun (somewhat?) introduction to the ES6 Generators. While, this post is not vast itself, it takes one of the most confusing topics of ES6 and introduces to its features in byte sized chunks. Also, there are links to some of the in depth articles which you must read to achieve a broader knowledge on this topic.
Study Material
Using ES6 Generators And Yield To Implement Asynchronous Workflow
ES6 Generators in depth by PonyFoo
ES6 Generators in depth by Dr. Axel Rauschmayer
The Hidden Power of Generators: Observable async flow control
Why can't anyone write a simple ES6 Generator tutorial
ES6 generators and async/await
And Finally, The Black Knight
All rights reserved