Начал читать «You Don't Know JS: Async and Performance» и застрял на
примере delegating recursion (кстати, как правильно перевести «delegating recursion»?). Прошел мысленно (и не только) по коду и получил верный результат, но не до конца понимаю промежуточные шаги, описанные в книге.
Пробовал расставлять
console.log()
внутри функций, пробовал дебаггером изучить call stack, но так и не могу привести в соответствие свое представление кода с книжным.
Функция
run()
принимает функцию-генератор, создает ее экземпляр и проходит его до конца, передавая каждое прошлое значение из
yield
в вызов
next()
.
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);
});
}
Код непонятного примера:
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);
Для удобства можно реализовать функцию
request()
так:
function request(url) {
return new Promise(function(resolve){
setTimeout(function(){
resolve( url.match(/v=(\d+)$/)[1] );
},1000);
});
}
Шаги, приведенные в книге (не перевожу, чтобы что-то не исказить):
run(bar)
starts up the *bar()
generator.
foo(3)
creates an iterator for *foo(..)
and passes 3
as its val
parameter.
- Because
3 > 1
, foo(2)
creates another iterator and passes in 2
as its val
parameter.
- Because
2 > 1
, foo(1)
creates yet another iterator and passes in 1
as its val
parameter.
1 > 1
is false
, so we next call request(..)
with the 1
value, and get a promise back for that first Ajax call.
- That promise is
yield
ed out, which comes back to the *foo(2)
generator instance.
- The
yield *
passes that promise back out to the *foo(3)
generator instance. Another yield *
passes the promise out to the *bar()
generator instance. And yet again another yield *
passes the promise out to the run(..)
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 the yield * into the *foo(3) instance, which then passes through the yield *
to the *foo(2)
generator instance, which then passes through the yield *
to the normal yield
that's waiting in the *foo(3)
generator instance.
- That first call's Ajax response is now immediately
return
ed from the *foo(3)
generator instance, which sends that value back as the result of the yield *
expression in the *foo(2)
instance, and assigned to its local val
variable.
- Inside
*foo(2)
, a second Ajax request is made with request(..)
, whose promise is yield
ed back to the *foo(1)
instance, and then yield *
propagates all the way out to run(..)
(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 local val
variable.
- Finally, the third Ajax request is made with
request(..)
, its promise goes out to run(..)
, and then its resolution value comes all the way back, which is then return
ed so that it comes back to the waiting yield *
expression in *bar()
.
Все понятно до 8-го шага
...which then passes through the yield *
to the normal yield
that's waiting in the *foo(3)
generator instance.
Почему
yield
, ожидающий в
foo(3)
, а не в
foo(2)
? Я думал, что после выполнения Промиса его значение (
1
) передается в строчку
return yield request( "http://some.url/?v=" + val );
и заменяет
yield
, то есть становится
return 1;
внутри
foo(1)
. Затем,
1
передается в
val = yield *foo( val - 1 );
, снова вместо
yield
, и получается
val = 1
внутри вызова
foo(2)
. После этого второй совершается второй вызов
request()
и через
yield
Промис передается в
foo(3)
. Затем
foo(3)
через
yield
предает Промис в
bar()
,
bar()
аналогично передает Промис в
run()
.
run()
ждет выполнения этого (второго) Промиса, так же как с первым, и то же самое дальше с третьим Промисом, только начиная внутри функции
foo(2)
со строчки
return yield request( "http://some.url/?v=" + val );
Что я упустил?