5 ms·
Coroutines in JavaScript for web components
- HumanOstrich 3y agoAlthough I tend to mostly use async/await these days, I find the other JavaScript asynchronous primitives interesting. Generator functions, iterables, and async versions of the two. The author shows a plausible way to put these primitives together to create coroutines powering rendering and lifecycles for web components. And although I'm pretty familiar with the syntax involved, I still learned some new ways to put it together from this article. Also the author's previous intro article to coroutines in JavaScript might be helpful for learning the basic syntax and patterns: https://lorenzofox.dev/posts/coroutine/ https://lorenzofox.dev/posts/coroutine/
- sroussey 3y agoI love how he used an EventTarget to simply the code.
- baxuz 3y agoWhat are some use cases of using generators to do coroutines? I honestly can't think of an example that couldn't be handled in a stateful way, by using objects/class instances instead of closures. Even iteration can be handled by implementing [Symbol.iterator] on the instance. The one place I found a use for them is to syncify async functions in some rare cases where it's needed, as the API would only work with other non-sync functions.
- postalrat 3y agoAlthough it doesn't really matter for much I've found closures to run faster than accessing class properties and found classes to use less memory than closures.
- athanagor2 3y agoHandling user inputs. Each time the generator yields you wait for a user input, and when you receive it you next() the generator with the input. It’s less verbose than using a bunch of methods, or the closest alternative I saw which is a switch/case on the index of the current operation. By using generators you just reuse the standard structures of the language.
- jkoudys 3y agoI'm the same camp. I use async/await because they exist and it's usually a good idea to use similar approaches to other devs, but I question why we even needed async/await in the first place. It's a big deal to add syntax to a whole language, and a small one to add a function to a library. We have const foo = async function() { const user = await fetchUser() But if they'd just made a Promise.coroutine, you could equivalently do this without needing to change the language: const foo = co(function* () { const user = yield fetchUser() There's some unpopular cases like async iterables or async generators, but for the most part we could've done the same thing without extending the language. I remember the v1 of koa that had yield everywhere and people thought it was confusing af. Then they released with await and suddenly it made sense to people.
- athanagor2 3y agoInteresting take. One could argue that by adding async functions on top of classic and generator ones we now have three different colors for the functions instead of just tw.
- wruza 3y agoAsync await is a syntactic sugar for functional continuations. These are “free” for C stacks, in a sense. Coroutines are not, because you have to yield and resume across C calls sometimes. The common argument for async await is that you avoid making system interfaces resumable. Another argument is that await explicitly marks yield-across points and there’s no sudden yield through an incomplete or potentially unsynchronized state.
- seer 3y agoI’ve tried to get into Streams at one point, for their promise of efficient processing of large (infinite) streams of data. Great for stuff like kafka / event streams. There is a lot of very nice functionality in Node around it - pipe functions and the like, and it can work, but in the end I’ve settled on just using async iterators. It’s just so easy and understandable to go for await (const item of my AsyncIterator) { … } And for people who don’t grasp in detail how it all works, the abstraction is very easy to use.
- rezonant 3y agoI use generators to handle performant asynchronous bitstream parsing. Using promises is far too slow: https://github.com/astronautlabs/bitstream https://github.com/astronautlabs/bitstream
- dfabulich 3y agoIf you enjoy this approach, you might enjoy the Crank JS framework. https://crank.js.org/ https://crank.js.org/ > Crank uses generator functions to define stateful components. You store state in local variables, and `yield` rather than `return` to keep it around.
- kwkelly 3y agoThe coolest thing (IMO) compared to react is the ability to await inside a component.
- threatofrain 3y agoI believe react has that now with the use hook.
- elwell 3y agoI love this for some reason. However, I wonder if it would end up being more complicated than React in practice. I generally prefer pedantic/stricter usage guidelines/conventions.
- kristiandupont 3y agoI started experimenting with this recently. Neither my knowledge nor the maturity is at a level where I would use it for production work but there are definitely some promising aspects to this. I am preparing a talk about it. The most significant unknown for me is how state management (like Redux etc.) would work in a larger app. I can think of many solutions but I can't figure out whether the tradeoffs then result in a net gain or loss.
- Alifatisk 3y agoI think this is my first time seeing techniques like “for ({} of this) {…}”, very interesting.
- ramesh31 3y ago> while(true) You're a braver man than I.
- AprilArcus 3y agoThis is a common idiom when working with generators, since iteration is controlled on the consuming side. Consider e.g. a trivial generator that counts up from one: function * makeSequence() { let i = 1; while (true) yield i++; } This function returns an iterator: const ints = makeSequence(); And now the caller is control of iteration, not the callee: function sumTo(cutoff) { const ints = makeSequence(); let sum = 0; for (let i = 0; i < cutoff; i++) { sum += ints.next().value; } return sum; } In this case there is no risk of an infinite loop.
- ramesh31 3y ago>This is a common idiom when working with generators, since iteration is controlled on the consuming side. I realize that, and it's why I (and most) have avoided the syntax entirely. Generators today feel more like a vestigial appendix to the language; an aborted branch of development, than anything I'd ever use or suggest.
- elwell 3y agoMore brave would be: while (0.1 + 0.2 !== 0.3)
- everforward 3y agoThings like that tend to be relatively easy to unit test as long as you have sane (read: few) exit conditions.
- russellbeattie 3y ago> if you try to advance a generator function while it is already advancing, you will get an error... Uncaught TypeError: already executing generator. Is this a good thing?
- spoiler 3y agoNot the author, but I believe the idea they want to illustrate is that you can't get into accidental render loops caused by the component itself
- chatmasta 3y agoI've been half-jokingly advocating for "resumable SSR" components that abuse wasm as an isomorphic, serializable coroutine runtime: start rendering a component inside a wasm VM on the server, while simultaneously serializing and streaming the state of the wasm VM to the client, and then resume rendering on the client. It's a bit of a meme that takes the idea of "React Server Components" to their logical extreme. And coding a PoC is beyond my capabilities... but I'd love to see someone do it.
- rezonant 3y agoThat's... a really cool idea
- jokethrowaway 3y agoSounds cool but in practice downloading a wasm bundle is bandwidth heavy and rendering to the dom in a browser is different than rendering to a string.
- kamikaz1k 3y agoHave you heard of Qwik framework? Not really you’re suggesting implementation wise, but they have their own idea for resumable rendering.
- Bellend 3y agoI really enjoyed the article! Generators have been around for maybe half a decade already but I never see them used. I am able to follow the article but have never seen an example at even small scale. All of the complexity for my software is still there in some form or other. I can have 100 classes it seems or 1000 functions and I really really struggle with breaking away from the imperative viewpoint. Especially when you have a small team consisting of seniors to juniors. I do love the pattern but no one else does. Why does it feel this way?
- masswerk 3y agoRegarding components, states and encapsulation, also mind the much ignored `Object.prototype.handleEvent`, which has been around since 1997. I, know, it's OO, but it is actually simple: function Counter(buttonElement) { this.clicks = 0; this.handleEvent = function(event) { switch (event.type) { case 'click': this.clicks++; console.log("I was clicked " + this.clicks + " times." ); break; //handle any other event types } }; buttonElement.addEventListener("click", this, false); } const myCounter = new Counter( someButtonElement ); (In case you wouldn't know: if an object is used as an event listener, its `handleEvent()` method will be called with that object bound to `this`. In a way, arrow functions are working around what had been already solved by this mechanism.)
- imbnwa 3y agoSadly React disallows this pattern even though this interface is in the DOM spec
- masswerk 3y agoIn the context of coroutines, the nice thing about this is that – as a catch-all trap – it works much like an interrupt that is internal to that specific object. (Of course, only when the GUI thread allows for event processing.) On the one hand, this is pretty much the essence of what events should be about: invoking a dormant object (or context) by the means of a message. On the other hand, this inherent locality may pose a problem for any framework. (And – in the age of AI generated images – on the third hand, this is why I personally don't like frameworks that much.)
- kamikaz1k 3y agoIt doesn’t disallow it, it’s just outside of how the framework operates. If you really want to make the two talk, you could, with the cost of having to maintain it.
- MrJohz 3y agoI'm not entirely sure what this adds in this case, though. For example, in the code you've written, as far as I can tell there's no advantage compared to defining the function as a closure variable and using it directly, rather than defining it as an attribute on `this` and passing the `this` object around. In addition, with the `handleEvent` approach, you need to handle all events within a single function. But it's fairly easy to create multiple functions within a single function scope and pass them to different event handlers, thus avoiding the need for the large (and potentially error-prone) switch statement if you end up needing to handle lots of events. Have you found cases where `handleEvent` works better than just defining local variables within a function and just using those? It seems to me that you wouldn't even need arrow functions to take advantage of the natural power of closures in this context.
- marcelr 3y agoGenerators are so cool I wrote my own ui/effects barebones runtime on it a few years back https://github.com/marcellerusu/capable-js https://github.com/marcellerusu/capable-js It’s especially cool when you have components that have distinct states they could be in, eg. a survey
- resoluteteeth 3y agoI think the concur web framework (originally for purescript but there's also a JavaScript version) is also based on generators