📜 ⬆️ ⬇️

Zone.js or how Dart saved Angular



I am a front-end developer at Wrike, writing in JavaScript and Dart, which is compiled into JavaScript. Today I want to talk about the Zone.js library, the underlying Angular 2.

Initially, Zone.js was invented by Google developers for the Dart programming language and the Dart2JS utility. With the help of this library, “Google users” solved the problem with the digest cycle, which was characteristic of the first Angular.
To understand where this library is used and what is needed, I ask for cat.

Problem


If you write in JavaScript or in languages ​​that are compiled into JavaScript, you probably have encountered the following situation:
working example :
')
var feedback = { message: '!', send: function () { alert(this.message) } } setTimeout(feedback.send) 

The problem is known for a long time - the context was lost. Therefore, in the pop-up message we will see "undefined". I know 4 ways to get out of the situation:


On this one could have finished, if not ...

Root of evil


The example we have considered is the tip of the iceberg, in fact the problem of loss of context is more global. The fact is that the context in JavaScript is lost in the place where the asynchronous functions are called, and in order to save it, you need to constantly keep the entire chain of calls in your head and control it. This is especially difficult in large projects.

There is an opposite effect: due to the loss of context, the connection with the place where the asynchronous function is called from is lost . For example, it is not clear where we hung the click handler that caused the error. In the console, only the link to the click handler itself is visible, but not shown where it is signed.

Decision


How many ways to call functions asynchronously do you know? SetTimeout, addEventListener, asynchronous requests to the server, etc. come to my mind. and so on. Not so much - the number of these places of course. What does it mean? If you prevent the loss of context for each asynchronous call, the problem will be solved. First, let's try to prevent the loss of context in setTimeout:

We write a class with a constructor and three methods.

 class Context { constructor(parentContext) { let context; if (parentContext) { //   context = Object.create(parentContext) context.parent = parentContext; } else { //    context = this; } return context; } fork() { //   return new Context(this); } bind(fn) { //    const context = this.fork(); //        return () => { return context.run(() => fn.apply(this, arguments), this, arguments); } } run(fn) { //      let oldContext = context; context = this; const result = fn.call() //     context = oldContext; //    return result; //   } } 

After that, substitute the original setTimeout:

 context = new Context(); var nativeSetTimeout = window.setTimeout; //  setTimeout context.setTimeout = (callback, time) => { callback = context.bind(callback); return nativeSetTimeout.call(window, callback.bind(context), time); }; window.setTimeout = function (){ return context.setTimeout.apply(this, arguments); }; 

Now client code:

 context.fork({} /*  ,  */).run(() => { context.message = '!'; setTimeout(() => { console.log(`%c  : «${context.message}»`, 'font-size: x-large'); }, 0); }); console.log(`%c  : «${context.message}»`,'font-size: x-large'); 

Now the link is saved. Here is an example of working code . Using the same technique, you can replace asynchronous calls for most cases and manually manage the context in a predictable manner.

But below is a graphic illustration of the work. Each zone is painted in its own color:

illustration of Zone.js work

In fact, we implemented part of the Zone.js library. I think that this can stop and do not write your bike, and continue to explore the zones, using the original library.

Library Description


After the library is connected, the Zone object appears in the global scope. Zone.current contains a link to the current zone. The Zone object's fork method returns a new zone based on the parent. It is better to look at what parameters are possible here in the documentation on github . The run method accepts a function whose body will execute within this zone. Here is an example .

 const childZone = Zone.current.fork({ name: ' ' }); const handler = () => { alert(`      «${Zone.current.name}»`); } childZone.run(handler); handler(); 

Library developers distinguish three types of asynchronous tasks:


Zone.js intercepts an attempt to schedule asynchronous tasks, perform callbacks, errors, and so on. Tasks are planned both explicitly, by calling special methods on the Zone.current object, and implicitly, by calling an asynchronous function (setTimeout), as we did in the first part of the article.

Zones are easy to combine: for example, one zone catches errors within its borders and sends notifications to the server, and a child zone (descendant) acts as a tracker and sends statistics on the user’s work in the graphical interface to the server. In this case, if an error occurs within the child zone, the parent zone will intercept and send information to the server. Here is an example of combining zones .

 const errorHandlerZone = Zone.current.fork({ name: 'ErrorZone', onHandleError: (parentZoneDelegate, currentZone, targetZone, error) => { sendError(error); return false; } }); const trackingZone = errorHandlerZone.fork({ name: 'TrackingZone' }); class Widget { render = () => { throw 'render error'; } } trackingZone.runGuarded(function(){ document.addEventListener('click', (event) => { trackEvent(event); }, true); const widget = new Widget(); widget.render(); return this; }); function sendError(error){ alert(`: ${error}  : ${Zone.current.name}`); } function trackEvent(event){ alert(`: ${event}  : ${Zone.current.name}`); } 

Other helpful examples



Long treysy


The first and rather typical example about which I wrote above is an error in the console. The click handler on the button fell and everything would be fine, that's just not clear where it was hung. With Zone.js, we can determine this. For this we use a special zone from the repository Zone.js. Here is an example .

Profiling


Using zones, we measure the running time of the code, which contains asynchronous calls, excluding non-code-related delays: timeouts, server response waiting, and pauses between events. An example .

And here Angular 2?


Zones are used in the second Angular. The framework understands that it is necessary to start the search engine for changes when an asynchronous event occurs. About the occurrence of this asynchronous event, he learns just from Zone.js.

If you present all of the above as a code, you get something like this:

 //   addEventListener function addEventListener(eventName, callback) { //   addEventListener callRealAddEventListener(eventName, function () { //    callback(...); //    var changed = angular2.runChangeDetection(); if (changed) { angular2.reRenderUIPart(); //   } }); } 

Thanks to zones, we know in which element an asynchronous event occurred. It remains to be seen whether the changes need to be rendered for children, and there is a distinctive feature of Angular 2. In the first Angular, we had to run a digest loop that went around the children and parents many times to check if the model had changed or not. The second Angular checks for changes unidirectionally.

disadvantages


Zone.js changes the standard behavior of the browser API ( overriding setTimeout is not good). This is a minus. Given that the monkey patching was done carefully, we used an anti-pattern . There are additional costs when calling the basic functions. These costs are small, but they are.

Mankipatching can lead to additional bugs in standard situations. Although I myself have never stumbled upon these bugs, they are potentially possible.

I also failed to make the zones work in the application on React and Angular of the first version as I wanted.

In fact, nothing prevents you from using Zone.js as such, but to make every component called in a separate zone is problematic. For full operation of zones, it is necessary that each piece of asynchronous code (event binding, asynchronous requests to the server) be called within the zone. I did not manage to manage these processes. The reaction uses virtual home and smart rendering with caching, and the basic events in Angular are hung in basic ngClick directives, which are tedious to rewrite. There is a chance that you will succeed. Share your successes in the comments.

Conclusion: Never Say Never


Zone.js is the case when skipping is appropriate. The advantages that the library provides override the shortcomings of the approach, and violation of the unwritten rules sometimes leads to victory.

Source: https://habr.com/ru/post/310422/


All Articles