I started reading You Don't Know JS: Async and Performance and tripped at delegating recursion example: I went through the code mentally and got the right result, but can't comprehend the description of the intermediate steps in the book.
Tried inserting console.log() into the functions' body, tried debugger to examine the call stack and still cannot conform my mental model of the code to the one, that's in the book.
function run(), which gets generator function as parameter, creates its instance and run it to the end, passing each previously yielded value to the next() call.
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// initialize the generator in the current context
it = gen.apply( this, args );
// return a promise for the generator completing
return Promise.resolve()
.then( function handleNext(value){
// run to the next yielded value
var next = it.next( value );
return (function handleResult(next){
// generator has completed running?
if (next.done) {
return next.value;
}
// otherwise keep going
else {
return Promise.resolve( next.value )
.then(
// resume the async loop on
// success, sending the resolved
// value back into the generator
handleNext,
// if `value` is a rejected
// promise, propagate error back
// into the generator for its own
// error handling
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})(next);
} );
}
the example code:
function *foo(val) {
if (val > 1) {
// generator recursion
val = yield *foo( val - 1 );
}
return yield request( "http://some.url/?v=" + val );
}
function *bar() {
var r1 = yield *foo( 3 );
console.log( r1 );
}
run( bar );
and for convenience's sake we can implement function request() like this:
function request(url) {
return new Promise(function(resolve){
setTimeout(function(){
resolve( url.match(/v=(\d+)$/)[1] );
},1000);
});
}
The book provides these steps:
run(bar)starts up the*bar()generator.foo(3)creates an iterator for*foo(..)and passes3as itsvalparameter.- Because
3 > 1,foo(2)creates another iterator and passes in2as itsvalparameter. - Because
2 > 1,foo(1)creates yet another iterator and passes in1as itsvalparameter. 1 > 1isfalse, so we next callrequest(..)with the1value, and get a promise back for that first Ajax call.- That promise is
yielded out, which comes back to the*foo(2)generator instance. - The
yield *passes that promise back out to the*foo(3)generator instance. Anotheryield *passes the promise out to the*bar()generator instance. And yet again anotheryield *passes the promise out to therun(..)utility, which will wait on that promise (for the first Ajax request) to proceed. - When the promise resolves, its fulfillment message is sent to resume
*bar(), which passes through theyield *into the*foo(3)instance, which then passes through theyield *to the*foo(2)generator instance, which then passes through theyield *to the normalyieldthat's waiting in the*foo(3)generator instance. - That first call's Ajax response is now immediately
returned from the*foo(3)generator instance, which sends that value back as the result of theyield *expression in the*foo(2) instance, and assigned to its localvalvariable. - Inside
*foo(2), a second Ajax request is made withrequest(..), whose promise isyielded back to the*foo(1)instance, and thenyield *propagates all the way out torun(..)(step 7 again). When the promise resolves, the second Ajax response propagates all the way back into the*foo(2)generator instance, and is assigned to its localvalvariable. - Finally, the third Ajax request is made with
request(..), its promise goes out torun(..), and then its resolution value comes all the way back, which is thenreturned so that it comes back to the waitingyield *expression in*bar().
Everything is clear until the 8th step.
...which then passes through the
yield *to the normalyieldthat's waiting in the*foo(3)generator instance.
Why waiting in foo(3), not in foo(2)? I thought after Promise fulfillment, its value (1) is passed to return yield request( "http://some.url/?v=" + val ); line, in place of yield, so we have return 1 at the end of foo(1). And then 1 is passed to val = yield *foo( val - 1 ); line, again, in place of yield, so we have val = 1 inside foo(2) call. After that, a second request() is made and yields a Promise to foo(3).
Then foo(3) yields the Promise to bar(), then bar() yields the Promise to run(). run() waits on the second Promise, just as with the first promise, and so on.
What have I overlooked?