You-Dont-Know-JS
You-Dont-Know-JS copied to clipboard
Async & Performance: optimize gen-runner in chapter 4
In the promise-aware gen runner, to prevent the unnecessary extra tick at the beginning, change:
return Promise.resolve().then(
function handleNext(value){ .. }
);
...to:
return Promise.resolve(
(function handleNext(value){ .. })()
);
Code also shown in chapter 4 of "ES6 & Beyond"
When I wrote that suggestion the deferring of a tick was intentional for clarity of code - but I don't have strong feelings about it.
The reason I don't like the extra tick is because an equivalent async function doesn't wait a tick to start, it runs its first part immediately.
@getify you're right :) I didn't know that would be the case when I first wrote that pump code.
Another part of the fact it's wrapped in a then provides throw safety which would need to be a try/catch - although I trust you know better than me what people might find easier to understand in the book.
the fact it's wrapped in a then provides throw safety which would need to be a try/catch
Ahh, you're absolutely right, totally forgot that was one of the motivations. That should have been commented. ;-)
OK, so we need this, huh?:
return Promise.resolve(
(function handleNext(value){
try {
var next = it.next(value);
// ..
}
catch (err) {
return Promise.reject(err);
}
})()
);
Pretty much, yeah, although then you can probably add a Promise.resolve on the value if the iterator is done and drop the Promise.resolve around everything.
the other (minor) downside is that this new try..catch is run (unnecessarily) on every subsequent step, not just the first step...it's unnecessary becasue the subsequent steps already have their own try..catch via the then(..). :/
FYI: I've previously adapted this gen-runner to use in another of my projects, CAF.
So just tweaked it based on our discussions here: https://github.com/getify/CAF/blob/94c60e2cdba714680f770ca04bd3fe2e5201b830/src/caf.src.js#L58-L98
Thoughts?
In particular, I'm curious/pondering if it's better (or different/broken) to move that outer catch(..) { .. } clause from line 94 up to line 65, and then just leave the return .. on line 66 outside of the try..catch? What do you think?
(edit): I thought moving the catch broke my tests, but it didn't. So I've made that change now: https://github.com/getify/CAF/blob/c084ebfeea8eabe28676f105700eb8575776024b/src/caf.src.js#L58-L98
Thoughts?
CAF looks really interesting, it's a shame we don't have proper cancel token support from the platform. I see you .return on the generator on cancellation which is what we did at bluebird and the reasonable thing to do IMO.
I would probably write it with an async function which would be really cute usage IMO:
function _runner(gen, ...args) {
const it = gen.apply(this, args);
async function result() {
let done = false, value = undefined;
while(!done) {
try {
({done, value}) = it.next(await value);
} catch (e) {
it.throw(e);
}
}
return value;
}
return {it, result() }
}
Although I'm not sure of its performance characteristics. What about the perhaps simpler:
function _runner(gen, ...args) {
const it = gen.apply(this, args);
function result(oldValue) {
let {done, value} = it.next(oldValue);
return Promise.resolve(value).then(result, it.throw.bind(it));
}
return {it, (async () => result())() } // the `async` lambda is to handle `throw`s
}
My main reason for not making it an async function is that my baseline target is ES6 support rather than ES2017 support. I don't like people to have to transpile my libraries if it's avoidable.
it's a shame we don't have proper cancel token support from the platform.
We do have the web platform's standard of AbortController, which CAF adheres to.
@getify What about that?
function run(gen, ...args) {
let it = gen(...args);
let next;
try {
next = it.next();
return handleValue(next);
} catch (err) {
return Promise.reject(err);
}
function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleValue);
}
function handleValue(next) {
if (!next.done) {
return Promise.resolve(next.value).then(nextStep, handleErr);
} else {
return Promise.resolve(next.value);
}
}
function nextStep(value) {
next = it.next(value);
return handleValue(next);
}
}