can-zone

By  on  

can-zone is a library that implements Zones.

Zones are an abstraction allowing you to write cleaner code for a variety of purposes, including implementing server-side rendered (SSR) applications, profiling, more useful stack traces for debugging, or a clean way to implement dirty checking.

This article will:

  • Explain what Zones are.
  • Explain how can-zone works.
  • Show can-zone's basic API.

Zones can be difficult to understand at first, so this article will stick to the basics. Next week I'll publish a follow-up article on blog.bitovi.com explaining how DoneJS uses can-zone to elegantly allow apps to be server-side rendered.

What are Zones?

As you already know, JavaScript is an asynchronous language. What this means in practice is that JavaScript engines contain (multiple) queues that they use to keep track of asynchronous tasks to be executed later. To think about this, take a look at a simple example of asynchronous code:

Async Example

This code runs a function, app, that schedules the function logging to be called twice with 2 different arguments. Breaking down what happens in the JavaScript engine:

  1. The script task is executed which defines and executes the app function. setTimeout is called twice, scheduling their callbacks to run after 10ms.

  2. After 10ms the first task will be taken from the queue and run to completion, logging 0 to 500.

  3. After the completion of the first task, the second task will be taken from the queue and run to completion. It will log from 0 to 5000.

  4. The task queue is now empty.

For a deeper dive into JavaScript tasks and microtasks check out Jake Archibald's post on the subject.

Zones provide a way to hook into the behavior of the JavaScript event loop. To better visualize what happens in the above code, see what happens when the same code is run in a Zone using can-zone.

Zone beforeTask and afterTask

Here we have the same code but with the addition of logging before and after each task runs. Notice that the first two things that are logged are "beforeTask" and "afterTask". This is because the running of app is, itself, a task. Then when the functions scheduled by the setTimeout are executed "beforeTask" and "afterTask" are logged for each of them as well.

With this building block we can create more useful abstractions for working with code that runs in an event loop. One that can-zone provides for you is the ability to know when all asynchronous tasks are complete. Each Zone has an associated Promise that will resolve when all tasks queues are emptied.

In the following example we have an application that performs two AJAX requests to display lists, and at the top the time it took to render. This can be written using Promises by waiting for all of the promises to resolve like below:

Frameworks

With only 2 asynchronous tasks to wait on this isn't so bad, but will scale poorly as the code becomes more complex (like if the requests were triggered as a side effect of some other function call). can-zone allows us to write this same code without manually keeping track of each request's promise:

Frameworks II

This tells us how long until the lists are fully displayed, but we can do better, and know how long it took for our code to actually execute, eliminating network latency from the equation. Using the Zone hooks discussed before, beforeTask and afterTask, we can measure just the time in which our JavaScript is executing:

Faster Load

This technique provides insight into why this code takes so long to render; it's not the fault of poorly written code but rather network latency is the problem. With that information we can make more informative optimizations for the page load time.

The concept of Zones is gaining steam in JavaScript. Angular has a similar Zone library. But while Angular's zone.js is aimed at aiding debugging and improving dirty checking code, can-zone is focused on solving server-side rendering.

How can-zone Works

In the future Zones might be part of the EMCAScript standard, but for now can-zone implements the behavior by wrapping functions that trigger asynchronous events (including XHR, setTimeout, requestAnimationFrame). can-zone not only wraps the functions, but also keeps count of when tasks complete, and provides a Promise-like API that lets you know when all asynchronous behavior has completed.

Above we saw some simple examples of Zones; below is a more complex example. It illustrates that even when asynchronous calls are nested inside of each other, can-zone will wait for everything to complete.

can zone

Under the hood, can-zone is overwriting the following methods:

  • setTimeout
  • clearTimeout
  • XMLHttpRequest
  • requestAnimationFrame
  • Promise
  • process.nextTick (in Node)
  • MutationObserver

It doesn't change their core behavior. It simply increments a counter to keep track of how many callbacks remain. The counter is decremented when those callbacks are called. When the count reaches zero, the Zone's Promise is resolved.

API and Features

Fine-grained control over which code you care about

Zone.ignore allow users to ignore (not wait on) certain functions. You might use this if you have code doing recursive setTimeouts (because that will never complete), or for some API call that is not important enough to wait on. Here's an example usage:

function recursive(){
  setTimeout(function(){
    recursive();
  }, 20000);
}

var fn = Zone.ignore(recursive);

// This call will not be waited on.
fn();

Zone.waitFor is a way to define custom asynchronous behavior. You can think of it as being the opposite of Zone.ignore. Let's say there is some asynchronous tasks that can-zone doesn't yet implement, or a Node library with with custom C++ bindings that do asynchronous things without our knowledge. You can still wrap these chunks of code to ensure they are waited on:

var Zone = require("can-zone");
var fs = require("fs");

module.exports = function(filename) {
  fs.readFile(__dirname + filename, "utf8", Zone.waitFor(function(err, file){
    Zone.current.data.file = file;
  }));
};

Lifecycle hooks

can-zone provides hooks to write code that runs at various points in the Zone lifecycle:

  • created - Called when the Zone is first created.
  • ended – Called when the Zone is about to resolve.
  • beforeTask – Called before each asynchronous task runs.
  • afterTask – Called after each asynchronous task runs.
  • beforeRun - Called immediately before the Zone's run function is executed.

These hooks are useful when implementing plugins. Earlier we created a simple performance plugin that used beforeTask and afterTask to time how long each task took to execute.

Create plugins

can-zone's constructor function takes a special config object called a ZoneSpec. The ZoneSpec object is where you:

  • Create callbacks for the lifecycle hooks.
  • Inherit behaviors from other plugins.
  • Define your own hooks that other plugins (that inherit from you) can provide callbacks for.
  • Define globals that should be overwritten in the Zone's async callbacks.

Here's an example of a plugin that changes the title of your page randomly.

var titleZone = {
  beforeTask: function(){
    document.title = Math.random() + " huzzah!";
  }
};

var zone = new Zone({
  plugins: [titleZone]
});

can-zone comes with a few of plugins you might find useful:

  • can-zone/xhr: Can be used on the server and client (assuming you have an XMLHttpRequest shim for Node) to provide caching capabilities when server-side rendering.
  • can-zone/timeout: Define a timeout, in milliseconds, at which time the Zone promise will be rejected.
  • can-zone/debug: Used in conjunction with can-zone/timeout, provides stack traces of each async task that failed to complete within the timeout.

More info

  • GitHub project page
  • jQuery-only can-zone SSR example with jQuery
  • NPM project page
  • Install it: npm install can-zone
Matthew Phillips

About Matthew Phillips

Matthew is an open-source developer for Bitovi, and a core contributor to DoneJS. Matthew's focus on DoneJS is module loading and server-side rendering.

Recent Features

  • By
    fetch API

    One of the worst kept secrets about AJAX on the web is that the underlying API for it, XMLHttpRequest, wasn't really made for what we've been using it for.  We've done well to create elegant APIs around XHR but we know we can do better.  Our effort to...

  • By
    Facebook Open Graph META Tags

    It's no secret that Facebook has become a major traffic driver for all types of websites.  Nowadays even large corporations steer consumers toward their Facebook pages instead of the corporate websites directly.  And of course there are Facebook "Like" and "Recommend" widgets on every website.  One...

Incredible Demos

  • By
    Generate Dojo GFX Drawings from SVG Files

    One of the most awesome parts of the Dojo / Dijit / DojoX family is the amazing GFX library.  GFX lives within the dojox.gfx namespace and provides the foundation of Dojo's charting, drawing, and sketch libraries.  GFX allows you to create vector graphics (SVG, VML...

  • By
    Flexbox Equal Height Columns

    Flexbox was supposed to be the pot of gold at the long, long rainbow of insufficient CSS layout techniques.  And the only disappointment I've experienced with flexbox is that browser vendors took so long to implement it.  I can't also claim to have pushed flexbox's limits, but...

Discussion

    Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!