JavaScript Async - DoEvents & Microtasks
Written by Ian Elliot   
Monday, 19 August 2019
Article Index
JavaScript Async - DoEvents & Microtasks
Reentrancy

DoEvents is generally regarded as a construct to be avoided. Like many such feared ideas it is only to be feared if you don't understand it. Correctly used it can be a simplification. This  extract from my recently published book JavaScript Async: Events Callbacks, Promises & Async/Await explains async and await makes DoEvents useful.

 

Now Available as a Print Book:

 JavaScript Async

cover

You can buy it from: Amazon

Contents

  1. Modern JavaScript (Book Only)
  2. Events,Standard & Custom
  3. The Callback
      extract - The Callback & The Controller
  4. Custom Async - setTimeout, sendMessage & yield
      extract - Custom Async
  5. Worker Threads
       extract - Advanced Worker Threads 
  6. Consuming Promises 
  7. Producing Promises
       extract - The Revealing Constructor Pattern
  8. The Dispatch Queue
       extract - Microtasks
  9. Async & Await
       extract -  Basic Async & Await
       extract -  DoEvents & Microtasks ***NEW
  10. Fetch, Cache & Service Worker
       extract - Fetch  
       extract - Cache

While async and await make asynchronous programming easy and less error-prone, if you are going to use it in creative or indeed in any way but the most basic, a thorough understanding what is going on is very important.

In the chapter but not in this extract:

  • Async Pretends To Be Sync
  • Basic Async
  • Basic Await
  • Where Does The Thread Go?
  • Async & Await Summary
  • Flow of Control
  • Error Handling

DoEvents – Microtasks

One very early attempt at taming asynchronous code is the doEvents command in Visual Basic. This simply yielded the UI thread and allowed the event queue to be processed. When all of the events had been processed the thread returned to the instruction after the doEvents command. This provided a very simply way for a long running function to keep the UI active.

The doEvents command came into some disrepute because of a lack of reentrancy control. An event handler could use a doEvents command only to be restarted because it has to handle another event in the queue. Of course there were ways to control this reentrancy problem, but overall doEvents was discouraged in favor of using tasks with separate threads.

The await command looks as if it might be a way to implement a doEvents command. It is, but there are some interesting problems in getting it to work and these reveal some of the more subtle details of how events are handled in modern JavaScript.

A simple minded doEvent function could simply return a resolved Promise that another function could await:

function doEvents() {
	return Promise.resolve();
}

Of course, as the Promise is resolved there is nothing to await, but it still causes the thread to be released to process the event queues.

So, for example, we could use doEvents to keep the UI active while using a loop to compute Pi:

button2.addEventListener("click",
 async function computePi() {
  var pi = 0;
  var k; 
  for (k = 1; k <= 10000000; k++) { 
   pi += 4 * Math.pow(-1, k + 1) / (2 * k - 1); 
   if (Math.trunc(k / 1000) * 1000 === k) {
     result.innerHTML = pi;
     count.innerHTML = k;
     await doEvents();
   }
  }	
);

If you try this out you will find that it doesn’t work. Sometimes you will see a partial update, but most often you will only see the final result after the function has finished running.

The reason that this doesn’t work is that, as described in the previous chapter, Promises make use of the microtask queue which is separate from the main task queue. Microtasks have a priority over tasks in the sense that the microtask queue is emptied after a task completes. What this means is that all Promises resolve and run their then method before the next full event is started. This is sometimes important to know, but the really important difference between the microtask queue and the task queue is that between tasks the browser may render the UI. What this means is that it is only when the full task queue is serviced does the UI have even a chance to render.

The solution to our non-working doEvents function is to make sure it causes the task queue to be processed. The simplest way to do this is to add an event to the queue using setTimeout. If you change doEvents:

function doEvents() {
  return pause(0);
}

where pause is the function given in the previous section that uses setTimeou:

function pause(t) {
  var promise = new Promise(
                 function (resolve, reject) {
			setTimeout(
			function () { 
			 	resolve();
			}, t); 
}); return promise; }

then you find it works and the UI is updated and remains responsive throughout the computation.

When you release the UI thread with an await, the event queue is only processed if there is an event waiting in the queue that relates to the returned Promise, and the UI is only updated when the event queue is processed. What this means is that you cannot simply return a resolved Promise and rely on the fact that Promises are always resolved asynchronously – in this case only the microtask queue is processed.

The only way to force the task queue to be processed with a Promise is to create a task that is needed to resolve the Promise.

For example you can use:

function doEvents() {
  var promise = new Promise(
	         function (resolve, reject) {
	          requestAnimationFrame(function () {
		    resolve();
	          });
		});
  return promise;
}

and this waits for the next animation frame before doing the update which is what we want.

coverasync



Last Updated ( Monday, 19 August 2019 )