Начал читать «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
yielded 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
returned 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 valvariable.
- Inside
*foo(2), a second Ajax request is made with request(..), whose promise is yielded 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 returned 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 );
Что я упустил?