Thursday, February 6, 2014

Taming Asynchronous Tasks in JavaScript with Zone.js

I recently learned about a new project by the Angular team called Zone. This project is one of those rare gems that is only a few lines of code but is so groundbreaking it literally takes time to wrap your mind around it. The easiest way to get started with Zone is to watch the excellent talk by its creator, Brian Ford. He demos some pretty cool scenarios that are all in the project repository.

In a nutshell, Zone provides what you might consider a “thread execution” context for JavaScript. It basically takes the current “context” you are in and intercepts all asynchronous events so they “map” to the same context. This enables you to do some pretty interesting things such as view stack traces through the original wire-up (i.e. you no longer get “disconnected” at the point the event was raised) or add tooling. You can use Zone to change the behavior of events (such as implementing your own version of setTimeout) and to keep track of tasks.

Zone works by simply running something in the context of the Zone. You can have multiple Zones with their own context. To steal an example from the Zone repo, consider this:

zone.run(function () {
  zone.inTheZone = true;

  setTimeout(function () {
    console.log('in the zone: ' + !!zone.inTheZone);
  }, 0);
});

console.log('in the zone: ' + !!zone.inTheZone);

Notice that you can set properties and call an asynchronous function and still have access to the properties in the context of the Zone but once you fall out you are no longer in the zone.

This gives rise to some very intriguing possibilities. As with all frameworks, the only way I knew I could wrap my arms around Zone is by building my own example so I’ll walk you through that and you can understand one of the many “use cases” for Zone.

I started with the idea of a simple HTML fragment that simply contains a button and an area to hold some data:

<div>
    <button id="myBtn">Populate</button>
    <span id="myData">Nothing</span>
</div>

Then I went old school with some JavaScript functions. I attach an event handler to the button and when you click it, it updates the data and sets a timer. The timer fires after 2 seconds and updates the data again. It looks like this:

var main = function () {
    
var btn = document.getElementById("myBtn"
),
         data = document.getElementById(
"myData"
);
     btn.addEventListener(
"click", function
() {
         data.innerHTML =
"Initializing..."
;
         setTimeout(
function
() {
             data.innerHTML =
"Done.";
         }, 2000);
     }); };

At this stage you can call the main method and see the app work. Let’s assume you wanted to time how long it is taking to wire things up (maybe you just invented a cool new data-binding framework, for example). How do you keep track of asynchronous events and capture them in the correct order? No worries. Don’t bother with writing that library. Just pull in Zone.

One nice thing about Zone is that you can create a template to intercept actions on the zone. You can store specific variables or functions on the zone, but more importantly you can implement functions that are called when the zone is entered and exited. This happens any time a function is executed in the context of the zone, including asynchronous functions. Borrowing from a performance example in the repo, I created a template for a profiling zone that picks up the best counter available to the browser:

var time = 0,
    
// use the high-res timer if available     timer = performance ?
             performance.now.bind(performance) :
             Date.now.bind(Date);

Then I return a zone template. Notice I created my own property called “marker” that I’m using to tag where I’m at. In the code I showed earlier, I’ll tag the zone by inserting zone.marker = “main” and zone.marker = “click” and zone.marker = “timeout” at the beginning of each function. This will give me some useful information later.

Here is the Zone template that I return:

return {
     marker:
"?"
,
     onZoneEnter:
function
() {
        
this.originalStart = this
.originalStart || timer();
        
this
.start = timer();
         console.log(
"Entered task"
);
     },
     onZoneLeave:
function
() {
        
var diff = timer() - this
.start,
             totalDiff = timer() -
this
.originalStart;
         console.log(
"Exited task " + zone.marker + " after "
+ diff);
         time += diff;
         console.log(
"Total active time: "
+ time);
         console.log(
"Total elapsed time: "
+ totalDiff);
     },
     reset:
function () {
         time = 0;
     } };

Now all I have to do is wrap things up in Zone. Because the context handles everything within in, there are no modifications to the main method. Zone will handle taking all of the events and marshaling them into my context for me. The only thing I need to do other than pulling in the Zone code itself is to call main like this:

zone.fork(profilingZone).run(function () {
     zone.reset();
     main(); });

This forks a copy based on my template and runs the main function in the context. Now I just run the example, wait a few seconds, and click the button. Here’s what shows up in my console. Note I did not modify my main method except to set the markers. All of the instrumentation was added automatically by the zone. Note total elapsed time is large because I literally ran this and waited as I authored the article before I clicked the button. That’s OK because the zone was able to keep track!  The difference between the last two total elapsed times is about 2 seconds or the interval I set on the timeout.

Entered task
Exited task main after 2.0000000076834112
Total active time: 2.0000000076834112
Total elapsed time: 2.0000000076834112
Entered task
Exited task click after 2.0000000076834112
Total active time: 4.0000000153668225
Total elapsed time: 830135.0000000093
Entered task
Exited task timeout after 0
Total active time: 4.0000000153668225
Total elapsed time: 832138.0000000063

How powerful is that? One common complaint about Angular is the digest loop that is used to scan models and listeners and look for changes in the model. It is required in order to respond to external changes and a special method must be called (apply) when you are mutating the model outside of this loop. Sometimes the loop must fire multiple times in order to be certain side effects are picked up (i.e. a change here causes a change there which means another place must be updated). Zone will enable the Angular team to fire the entire cycle inside a zone context and the team believes that will eliminate the need for digest or apply entirely!

To see this for yourself, check out this fiddle. I don’t know of a CDN for Zone yet so I just included the entire source in the fiddle.