JavaScript Async - Microtasks |
Written by Ian Elliot | |||
Sunday, 26 November 2017 | |||
Page 1 of 2 The workings of the JavaScript dispatch queue are more subtle and interesting than you might think. Far from just being a queue of events, there are tasks and then there are sub-tasks. This is an extract from the newly published JavaScript Async: Events Callbacks, Promises & Asnc/Await
Now Available as a Book:JavaScript AsyncYou can buy it from: Amazon Contents
Having met the Promise in earlier chapters, we have all that we need to examine the dispatch queue in more detail. Many of the ideas in this chapter have been introduced earlier, but now is a good time to explain the deeper ideas and some alternatives. Tasks and MicrotasksOur earlier description of the event or dispatch queue is slightly over-simplified. Now that we have explored many of the possible ways of working with the queue it is time to find out some finer points of how it works. It is clear that each thread of execution gets its own queue. All of the windows from a single origin share the same queue, and this is used by the postMessage method to allow them to communicate. Tasks are originally the only thing placed in the event queue, but modern JavaScript and browsers also now support microtasks. The event loop takes tasks one at a time and allows the task to run to completion before the next task is serviced. Tasks can also be preferentially dequeued according to their source. This allows the system to prioritize some tasks. A subtle, but sometimes important point is that the browser may render updates to the page between servicing tasks. This is important because if your code makes changes to the page you will generally only see these changes if you allow the UI thread to service the queue and there has to be a task waiting for it to perform the update. Microtasks were introduced partly as a lightweight version of the task, but more to introduce a new class of task that was guaranteed to be executed before the next task starts i.e. as soon as possible.
That is the microtask queue is processed after each task has completed. Any microtasks that are created while the microtask queue is being processed are added to the queue and processed before moving on to the next task. Also no page updates are performed until the next task is processed. This means that microtasks aren’t slowed down by page rendering. The big problem with this task/microtask split is that browsers aren’t consistent in the way that they implement it. The reason is that ECMAScript describes things in a different way to HTML 5. ECMAScript has jobs which are the same thing as microtasks. At the moment the only way to find out with certainty if something is placed in the task or the microtask queue is to write a program to test what order things happen in. The important points are:
The sequence is: task ends→ Notice that if the UI thread is freed and there are no tasks to process, the microtask queue is still cleared. In general older features, events and methods like setTimeout work by adding tasks to the queue. Newer features that need to be processed rapidly like Promise settling add microtasks to the queue. At the moment the only things that use microtasks in browser-based JavaScript are the Mutation observer and the Promise. The Promise object adds a microtask to the queue for each of its callbacks. It is also worth keeping in mind that not all browsers support microtasks. In most cases you can ignore the distinction between task and microtask, but just occasionally you will encounter some unexpected behavior which doesn’t make sense unless you know about microtasks. Adding a MicrotaskOne of the recurring problems in JavaScript is finding a way to add something to the dispatch queue. For reasons that are difficult to work out there is resistance to adding commands that directly interact with the queue and this leaves JavaScript programmers with the responsibility of inventing ways to do the job with the commands they have. For example for tasks the most obvious way is to use setTimeout, although this isn’t a very good solution. For microtasks there is a more direct way, but it relies on the JavaScript engine implementing Promises correctly as microtasks – most do, but with slight variations in implementation. To add a microtask to the queue all you have to do is:
The Promise.resolve method returns a resolved Promise object and its then method can be used to add a function to the microtask queue. Recall that even for a resolved Promise its callbacks are executed asynchronously. We can try this out and demonstrate the way that microtasks behave with a simple program:
This uses setTimeout to add two tasks and Promise.resolve to add two microtasks. The order that they are added in suggests that we should see:
but on the current version of Chrome, Edge and Firefox what we see is:
You can add as many microtasks and tasks as you like and you will see all of the microtasks executed before the tasks. You might be wondering why Task1 was printed first. The reason is that the microtask queue is emptied first when the code that sets the tasks/microtasks ends. In principle microtasks are always processed first. You can use this program as a test to see if an object is queuing a task or a microtask. Simply replace the Promise.resolve by whatever you want to test. Promises Are Not Fully AsyncYou may have noticed that there is a small problem with microtasks hidden in the description that the microtask queue is emptied before the next task and any microtasks that are generated while it is being processed are added to the queue and executed. The general assumption is that Promises are asynchronous and this in turn implies that they are non-blocking and executing any number of short Promise callbacks cannot block the UI thread because it is serviced between each one. This isn’t true. No tasks are processed while the microtask queue is being emptied. For example:
If you run this you will see “MicroTask” 100 times and then “Task”. The 100 items on the microtask queue blocks the servicing of the task queue until it is emptied. This effectively freezes the UI for the duration. If you would like to see what happens simply increase number of repeats to 10,000 and you will discover that the UI freezes for that time. |
|||
Last Updated ( Monday, 27 November 2017 ) |