Hello once again to Readers’ Questions, presented by the Ember.js Times. Today I’m answering this question:
Why does Ember ship with RSVP instead of a Promise polyfill?
Well, RSVP.Promise
is a spec-compliant Promise polyfill, and Ember folks even helped write that spec based on early adoption experience in Ember. But you are probably asking why it is more than just a Promise polyfill, and why we don’t always use native Promise where it’s available, and only fall back to RSVP where it’s not.
Ember (and every other modern Javascript rendering framework) batches up your changes before rendering them out to the DOM. If we didn’t do this, every time you called set
to change something, we would instantly rerender the DOM, and when you are calling set
many times in a loop that would get expensive quickly.
So instead, whenever you change some data we schedule rendering for “after all your current work is done”. And there are (for purposes of this discussion) two different kinds of “after” in Javascript: tasks and microtasks.
In this example:
setTimeout(() => console.log("setTimeout fired"), 0);
Promise.resolve().then(() => console.log("promise resolved"));
a correct Javascript implementation should always print “promise resolved” before “setTimeout fired”, because resolved promises are scheduled on the microtask queue and setTimeout
is scheduled on the task queue. The browser is not allowed to move on to the next task until all the microtasks are done. That is the purpose of microtasks – they let you continue your current work later-ish, in the sense of being asynchronous, but still guarantee that it will get grouped together before any other task begins.
When you have microtasks, the problem of scheduling render for “after I finish all my current work” is relatively straightforward: you do all your “current work” using microtasks, and you schedule the rerender with a task. Thus, you’re guaranteed that the render won’t happen until all the currently running work finishes.
But up until Ember 3.0 (February 14, 2018) we supported browsers that didn’t have reliable, correct microtask scheduling. So Ember has long had it’s own built-in implementation of this same pattern: the runloop. The runloop achieves the same thing you would with the native microtask queue: it groups together work that’s supposed to go together, and lets you schedule what should happen “after” all that work has finished.
In order to know when “after” is, the runloop needs to be aware of all the asynchronous work that’s going on. So in order for the runloop and promises to interoperate, Ember configures RSVP.Promise
to not use the native microtask queue, but instead to delegate to the runloop. Therefore in Ember apps, RSVP.Promise
doesn’t behave exactly the same as native Promise
, and that’s why we continue to use it.
The good news is, now we are post-ember-3.0 and we can trust native microtask queues in all supported browsers. Switching over to the native microtask queue is not a large amount of work, though it needs to be done carefully. Two attempts have already been made to merge this fix into Ember, and both times we found serious bugs and reverted to try again, rather than break people’s apps. I think third time’s a charm.
Embracing the autorun
When most Ember developers hear “run loop”, they probably think of the dreaded autorun assertion:
You have turned on testing mode, which disabled the run-loop’s autorun. You will need to wrap any code with asynchronous side-effects in a run.
which sometimes forces them to manually call run
to establish the start and end of the run loop, which is super annoying.
The run loop has always been capable of automatically starting itself (this is an “autorun”), but starting a new run loop automatically can cause subtle timing changes and make your tests fail in very confusing ways. The problem with our classic autorun is that it can only detect “when things are done” by scheduling a new task (a setTimeout
), and there’s no way to stop other tasks from intervening between when you schedule your work and when the work is done. This is in contrast with explicit run loops (which Ember uses internally almost exclusively, and which users sometimes add to their code via Ember.run
), which have an explicit beginning and end, so that they can guarantee that they finish their work before any other tasks (including things like click events or xhr handlers) can intervene.
But now that we can leverage the native microtask queue, we can make autoruns as robust as explicit run loops. The question of when to start and end the loop goes away – the native microtask queue is just always present. It’s always safe to schedule more work onto it, and rely on a native-task-based flush that will take care of all the “after” work. So the plan is to remove the assertion and just embrace autoruns, because they become no longer a problem.
No more run loop?
Does this mean the run loop is going away? Not really – it’s still a thing that you want to exist behind the scenes, reliably making sure there’s always a render scheduled to happen at the right time to keep your work optimally batched. But the runloop as something that app developers will need to learn about just to get their app working & tested can definitely go away, and that will make me very happy.
That is also the point where RSVP.Promise
and native Promise
will be on equal footing, and there will be no reason not to just use native promises everywhere.
What about async functions?
The async
and await
keywords are “the best thing to happen to modern javascript”, says @rwjblue. And I agree.
There are a lot of places where you can already take advantage of them in Ember, particularly in our modern-style tests. But making them work everywhere is the same as making native Promises work everywhere, because they are just syntactic sugar for native promises. So everything above about Ember soon working with native promises also applies to async
/ await
.
Further reading
For a good introduction to microtasks and tasks (along with some detail on how bad browsers were at consistently supporting them until recently) check out Tasks, microtasks, queues and schedules by Jake Archibald
I have often referred back to Chris Thoburn’s (@runspired) thorough presentation on scheduling in Ember, “Beyond 60 FPS” (video, slides). It includes illustrated examples of how the task and microtask queues work.
Here is the bug that caused us to twice revert switching Ember to native microtask queue.