can-zone
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 ExampleThis 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:
The script task is executed which defines and executes the
app
function. setTimeout is called twice, scheduling their callbacks to run after 10ms.After 10ms the first task will be taken from the queue and run to completion, logging 0 to 500.
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.
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 afterTaskHere 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:
FrameworksWith 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 IIThis 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 LoadThis 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 zoneUnder 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
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.