📜 ⬆️ ⬇️

Make your AngularJS: Part 1 - Scope and Digest

Angular is a mature and powerful JavaScript framework. It is quite large and is based on a multitude of new concepts that you need to master in order to work with it effectively. Most developers, getting acquainted with Angular, face the same difficulties. What exactly does the digest function do? What are some ways to create directives? What is the difference between service and provider?

Despite the fact that Angular has pretty good documentation , and there are a lot of third-party resources , there is no better way to learn the technology than to disassemble it in pieces and reveal its magic.

In this series of articles, I 'm going to recreate AngularJS from scratch . We will do this together step by step, in the process of which you will understand the internal structure of Angular much deeper.

In the first part of this series, we will consider the device scope, and how, in fact, $ eval , $ digest, and $ apply work . Checking the data for change (dirty-checking) in Angular seems like magic, but it’s not - you will see for yourself.
')

Training


Project sources are available on github, but I would not advise you to just copy them yourself. Instead, I insist that you do everything yourself, step by step, playing around with the code and digging into it. In the text I use JS Bin , so you can work through the code without even leaving the page ( note. Lane - only references to JS Bin code will be translated).

We will use Lo-Dash for some low-level operations with arrays and objects. Angular itself does not use Lo-Dash, but for our purposes it makes sense to remove as much of the template low-level code as possible. Wherever you meet in the code ( _ ) (underscore character) - Lo-Dash functions are called.

We will also use the console.assert function for the simplest checks. It should be available in all modern JavaScript environments.

Here is an example of Lo-Dash and assert in action:

JS Bin Code
View code
var a = [1, 2, 3]; var b = [1, 2, 3]; var c = _.map(a, function(i) { return i * 2; }); console.assert(a !== b); console.assert(_.isEqual(a, b)); console.assert(_.isEqual([2, 4, 6], c)); 

Console:
 true
 true
 true 


Objects - scope (s)


Objects of scope in Angular are regular JavaScript objects to which you can add properties in the standard way. They are created using the Scope constructor. Let's write its simplest implementation:

 function Scope() { } 

Now, using the new operator, you can create scope-objects and add properties to them.

 var aScope = new Scope(); aScope.firstName = 'Jane'; aScope.lastName = 'Smith'; 

These properties are nothing special. No need to assign any special setters (setters), there are no restrictions on the types of values. Instead, all the magic lies in two functions: $ watch and $ digest .

Monitoring object properties: $ watch and $ digest


$ watch and $ digest are two sides of the same coin. Together they form the core of what scope objects are in Angular: the response to data changes.

Using $ watch you can add to the scope of the “observer”. The observer is what will be notified when a change occurs in the appropriate scope.

An observer is created by passing two functions to $ watch :


In Angular, instead of the watch functions, you usually used a watch expression. This is a string (something like “user.firstName”) that you specified in html when linking, as an attribute of a directive, or directly from JavaScript. This line was parsed and compiled by Angular in, similar to our watch-function. We will look at how this is done in the next article. In the same article, we will stick to the low-level approach using watch functions.

To implement $ watch , we need to store all registered observers somewhere. Let's add an array for them to the Scope constructor:

 function Scope() { this.$$watchers = []; } 

The $$ prefix means that the variable is private in the Angular framework and should not be called from the application code.

Now you can define the function $ watch . It will take two functions as arguments and store them in the $$ watchers array. It is supposed that every scope-object needs this function, so let's put it into a prototype:

 Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); }; 

The downside is the $ digest function. It launches all observers registered for a given scope. Let us describe its simplest implementation, in which all observers simply move, and each of them has a listener function called:

 Scope.prototype.$digest = function() { _.forEach(this.$$watchers, function(watch) { watch.listenerFn(); }); }; 

Now you can register an observer, run $ digest , as a result, it will work out its listener function:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { _.forEach(this.$$watchers, function(watch) { watch.listenerFn(); }); }; var scope = new Scope(); scope.$watch( function() {console.log('watchFn'); }, function() {console.log('listener'); } ); scope.$digest(); scope.$digest(); scope.$digest(); 

Console:
 "listener"
 "listener"
 "listener" 


By itself, this is not particularly useful. What we would really like is for the handlers to be launched only if the data indicated in the watch functions really changed.

Data change detection


As mentioned earlier, the watch function of the observer must return the data, the change of which is interesting to it. Usually this data is in scope, therefore, for convenience, the scope is passed to it as an argument. A watch function that looks at the firstName of scope will look something like this:

 function(scope) { return scope.firstName; } 

In most cases, the watch function looks like this: retrieves the data that it is interested in from the scope and returns it.

The function of $ digest is to call this watch-function and compare the value received from it with what it returned last time. If the values ​​differ, then the data is “dirty” and you need to call the appropriate listener function.

To do this, $ digest must remember the last returned value for each watch function, and since we already have an object for each observer, it is most convenient to store this data in it. Here is the new implementation of the $ digest function, which checks for change data for each observer:

 Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; 

For each observer, a watch function is called, passing the current scope as an argument. Next, the resulting value is compared with the previous one, stored in the last attribute. If the values ​​differ, the listener is called. For convenience, both values ​​and scope are passed as arguments to the listener. At the end, the last value of the observer has a new value written so that it can be compared next time.

Let's take a look at how listeners are started in this implementation when $ digest is called:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // We haven't run $digest yet so counter should be untouched: console.assert(scope.counter === 0); // The first digest causes the listener to be run scope.$digest(); console.assert(scope.counter === 1); // Further digests don't call the listener... scope.$digest(); scope.$digest(); console.assert(scope.counter === 1); // ... until the value that the watch function is watching changes again scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2); 

Console:
 true
 true
 true
 true 


Now we have already implemented the core of the Angular-ovshe scope: registering observers and launching them in the $ digest function.

We can also now draw a couple of conclusions regarding the performance of the scope in Angular:


Alert about what is happening digest


If you need to receive notifications that $ digest is running, you can take advantage of the fact that each watch function, in the process of running $ digest, is sure to start. You just need to register the watch-function without a listener.

To take this into account, in the $ watch function, it is necessary to check whether the listener is missing or, if yes, to replace the stub function in its place:

 Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; 

If you use this template, keep in mind that Angular takes into account the value returned from the watch function, even if the listener function is not declared. If you return any value, it will participate in the check for changes. In order not to cause unnecessary work, just don’t return anything from the function, by default undefined will always be returned:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.$watch(function() { console.log('digest listener fired'); }); scope.$digest(); scope.$digest(); scope.$digest(); 

Console:
 "digest listener fired"
 "digest listener fired"
 "digest listener fired" 


The kernel is ready, but it’s still far from the end. For example, a fairly typical scenario is not taken into account: listener functions themselves can change properties from scope. If this happens, and another observer was following this property, it may happen that this observer will not receive notification of the change, at least during this $ digest pass:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {} }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.counter; }, function(newValue, oldValue, scope) { scope.counterIsTwo = (newValue === 2); } ); scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // After the first digest the counter is 1 scope.$digest(); console.assert(scope.counter === 1); // On the next change the counter becomes two, but our other watch hasn't noticed this yet scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2); console.assert(scope.counterIsTwo); // false // Only sometime in the future, when $digest() is called again, does our other watch get run scope.$digest(); console.assert(scope.counterIsTwo); // true 

Console:
 true
 true
 false
 true 


Let's fix it.

We execute $ digest as long as there is “dirty” data


It is necessary to correct the $ digest so that it continues to do checks until the observed values ​​stop changing.

First, let's rename the current $ digest function into $$ digestOnce , and change it so that, running all watch functions once, returns a boolean indicating whether there was at least one change in the values ​​of the observed fields or not:

 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; 

After that, we will re-declare the $ digest function so that it runs $$ digestOnce in the loop as long as there are changes:

 Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); }; 

$ digest now performs registered watch functions at least once. If, in the first pass, any of the observed values ​​has changed, the pass is marked as “dirty” and the second pass is started. This happens until no altered value is detected for the entire passage - the situation stabilizes.
Angular scope s do not really have the $$ digestOnce function. Instead, this functionality is built into the loop directly in $ digest. For our purposes, clarity and readability are more important than performance, so we did a little refactoring.

Here is the new implementation in action:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn ||function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.counter; }, function(newValue, oldValue, scope) { scope.counterIsTwo = (newValue === 2); } ); scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // After the first digest the counter is 1 scope.$digest(); console.assert(scope.counter === 1); // On the next change the counter becomes two, and the other watch listener is also run because of the dirty check scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2); console.assert(scope.counterIsTwo); 

Console:
 true
 true
 true 


You can make another important conclusion regarding the watch-functions: they can work several times in the process of $ digest. That is why, it is often said that watch functions should be idempotent: there should be no side effects in the function, or there should be such side effects for which it will be normal to trigger several times. If, for example, in a watch function, there is an AJAX request, there are no guarantees on how many times this request is executed.

There is one big flaw in our current implementation: what happens if two observers watch each other's changes? In this case, the situation never stabilizes? A similar situation is implemented in the code below. In the example, the call to $ digest is commented out.

Uncomment it to find out what happens:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); }; var scope = new Scope(); scope.counter1 = 0; scope.counter2 = 0; scope.$watch( function(scope) { return scope.counter1; }, function(newValue, oldValue, scope) { scope.counter2++; } ); scope.$watch( function(scope) { return scope.counter2; }, function(newValue, oldValue, scope) { scope.counter1++; } ); // Uncomment this to run the digest // scope.$digest(); console.log(scope.counter1); 

Console:
 0 


JSBin stops the function after a while (about 100,000 iterations occur on my machine). If you run this code, for example, under node.js, it will run forever.

Getting rid of instability in $ digest


All we need is to limit the work of $ digest to a certain number of iterations. If the scope still continues to change after the end of the iterations, we raise our hands and give up - probably the state never stabilizes. In this situation, an exception could be thrown, since the state of the scope is clearly not what the user expected it to be.

The maximum number of iterations is called TTL (abbreviation from time to live - life time). By default, we set it to 10. This number may seem small (we just started the digest about 100,000 times), but note that this is a performance issue - the digest is performed frequently, and every time all watch functions are processed in it. In addition, it seems unlikely that the user will have more than 10 watch-related functions.
In Angular TTL can be customized. We will come back to this in the next articles, when we discuss providers and dependency injection.

Well, let's continue - let's add a counter to the digest loop. If you reach the TTL - throw an exception:

 Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; 

An updated version of the previous example throws an exception:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.counter1 = 0; scope.counter2 = 0; scope.$watch( function(scope) { return scope.counter1; }, function(newValue, oldValue, scope) { scope.counter2++; } ); scope.$watch( function(scope) { return scope.counter2; }, function(newValue, oldValue, scope) { scope.counter1++; } ); scope.$digest(); 

Console:
 "Uncaught 10 digest iterations reached (line 36)" 


From obsession with digest got rid of.

Now let's look at exactly how we determine that something has changed.

Check for change by value


At the moment, we compare the new values ​​with the old ones, using the strict equality operator === . In most cases it works: the change is normally determined for primitive types (numbers, strings, etc.), it is also determined if the object or array is replaced by another. But in Angular, there is another way to identify changes, it allows you to find out if something has changed inside the array or object. To do this, compare by value , not by reference .

This type of check can be enabled by passing an optional third Boolean type parameter to the $ watch function. If this flag is true, then check by value is used. Let's modify the $ watch - we will receive the flag and store it in the observer ( watcher variable):

 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; 

All we did was add a flag to the observer, forcing it to a boolean type, using double negation. When the user calls $ watch without the third parameter, valueEq will be undefined , which is converted to false in the watcher object.

Check by value implies that if the value is an object or an array, you will need to go over both the old and the new content. If there are any differences, the observer is marked as “dirty”. The contents may contain nested objects or arrays, in which case they will also need to be recursively checked by value.

Angular has its own comparison function by value , but we will use the one in Lo-Dash . Let's write a comparison function that accepts a pair of values ​​and a flag:

 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue; } }; 

In order to determine the changes “by value”, it is also necessary for the friend to keep the “old values”. It is not enough just to keep references to current values, since any changes that are made will also get into the object that we stored. We will not be able to determine whether something has changed or not if the $$ areEqual function always has two references to the same data. Therefore, we will have to do a deep copy of the content, and save this copy.

Just as in the case of the comparison function , Angular has its own function of deep data copying , but we use the same from Lo-Dash . Let's modify $$ digestOnce to use $$ areEqual for comparison and make a copy in the last if needed:

 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; 

Now you can see the difference between the two ways of comparing values:

JS Bin Code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue; } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.counterByRef = 0; scope.counterByValue = 0; scope.value = [1, 2, {three: [4, 5]}]; // Set up two watches for value. One checks references, the other by value. scope.$watch( function(scope) { return scope.value; }, function(newValue, oldValue, scope) { scope.counterByRef++; } ); scope.$watch( function(scope) { return scope.value; }, function(newValue, oldValue, scope) { scope.counterByValue++; }, true ); scope.$digest(); console.assert(scope.counterByRef === 1); console.assert(scope.counterByValue === 1); // When changes are made within the value, the by-reference watcher does not notice, but the by-value watcher does. scope.value[2].three.push(6); scope.$digest(); console.assert(scope.counterByRef === 1); console.assert(scope.counterByValue === 2); // Both watches notice when the reference changes. scope.value = {aNew: "value"}; scope.$digest(); console.assert(scope.counterByRef === 2); console.assert(scope.counterByValue === 3); delete scope.value; scope.$digest(); console.assert(scope.counterByRef === 3); console.assert(scope.counterByValue === 4); 

:
 true
 true
 true
 true
 true
 true 


, , , . , . Angular . .
Angular : “ ”. , , , . . $watchCollection — .

JavaScript.

NaN


JavaScript NaN (not a number — ) . , , . NaN , watch-, NaN , “”.

“ ” isEqual Lo-Dash. “ ” . $$areEqual :

 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; 

NaN :

JS Bin
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.number = 0; scope.counter = 0; scope.$watch( function(scope) { return scope.number; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1); scope.number = parseInt('wat', 10); // Becomes NaN scope.$digest(); console.assert(scope.counter === 2); 

:
 true
 true 


Now let's shift the focus from checking the values ​​to how we can interact with the scope from the application code.

$ eval - code execution in scope


In Angular, there are several options for running code in the context scope. The simplest of these is the $ eval function . It takes a function as an argument, and the only thing that does is call it immediately, passing it the current scope, as a parameter. Well, and then it returns the result of the execution. $ eval also accepts a second parameter, which it passes unchanged to the function being called.

The implementation of $ eval is very simple:

 Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; 


Using $ eval is also quite simple:

JS Bin code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; var scope = new Scope(); scope.number = 1; scope.$eval(function(theScope) { console.log('Number during $eval:', theScope.number); }); 

:
"Number during $eval:"
 one 


So what's the use of such an elaborate way of calling a function? One advantage is that $ eval makes the code that works with the contents of the scope a little more transparent. Also $ eval is a building block for $ apply , which we will deal with soon.

However, the greatest benefit of $ eval will manifest itself only when we begin to discuss the use of “expressions” instead of functions. As with $ watch , you can pass a string expression to the $ eval function . It will compile it and execute in the context scope. Further in the series of articles, we will implement this.

$ apply - integration of external code with the $ digest loop


, $apply Scope . , c Angular. .

$apply , , $eval , $digest . :

 Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } }; 

$ digest is called in the finally block to update the dependencies, even if exceptions occurred in the function.

The idea is that using $ apply , we can execute code that is not familiar with Angular. This code can change the data in the scope, and $ apply will ensure that the observers catch these changes. This is what they mean when they talk about "integrating code into the Angular life cycle." This is nothing more.

$ apply in action:

JS Bin code
View code
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } }; var scope = new Scope(); scope.counter = 0; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$apply(function(scope) { scope.aValue = 'Hello from "outside"'; }); console.assert(scope.counter === 1); 

:
 true 



Delayed Execution - $ evalAsync


JavaScript «» — , . SetTimeout() ( ) .

Angular , $timeout , digest- $apply .

Angular — $evalAsync . , , digest ( ), digest-. , , - listener- , , , , , digest-.

, , $$evalAsync . , Scope :

 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; } 

$evalAsync , :

 Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); }; 

scope (scope-), .

, $digest , , , , $eval :

 Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; 

, , scope , “” — , digest-.

, $evalAsync :

JS Bin
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); }; var scope = new Scope(); scope.asyncEvaled = false; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; scope.$evalAsync(function(scope) { scope.asyncEvaled = true; }); console.log("Evaled inside listener: "+scope.asyncEvaled); } ); scope.aValue = "test"; scope.$digest(); console.log("Evaled after digest: "+scope.asyncEvaled); 

:
"Evaled inside listener: false"
"Evaled after digest: true" 


scope


$evalAsync -, digest, . , $evalAsync , , « », , - digest.

$evalAsync - , digest . Angular scope , «», scope, , .

$scope $$phase , null :

 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$phase = null; } 

Next, let's write a couple of functions to control the phase: one for installation, the other for cleaning. We also add an additional check that no one is trying to establish a phase without completing the previous one:

 Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; 

In the $ digest function, set the “$ digest” phase, wrap the digest loop in it:

 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); }; 

While we are here, let's finalize $ apply at the same time so that the phase is also prescribed here. This will be useful during the debugging process:

 Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; 

Finally, you can now schedule a call to $ digest in the $ evalAsync function . Here you will need to check the phase, if it is empty (and no asynchronous tasks have yet been scheduled) - we plan to execute $ digest :

 Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; 

, $evalAsync , , digest , , :

JS Bin
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; var scope = new Scope(); scope.asyncEvaled = false; scope.$evalAsync(function(scope) { scope.asyncEvaled = true; }); setTimeout(function() { console.log("Evaled after a while: "+scope.asyncEvaled); }, 100); // Check after a delay to make sure the digest has had a chance to run. 

:
"Evaled after a while: true" 


digest — $$postDigest


There is another way to add your code to the execution stream of a digest loop - using the $$ postDigest function .

The double dollar at the beginning of the function name indicates that this is a deep Angular function that Angular application developers should not use. But it doesn’t matter to us, we still implement it.

Like $ evalAsync , $$ postDigest allows you to postpone the launch of some code to “later”. More specifically, the deferred function will be executed immediately after the next digest is completed. Using $$ postDigest does not imply the launch of $ digest , , - digest. , $$postDigest - digest, scope , $$postDigest , $digest $apply , .

, Scope , $$postDigest :

 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } 

, $$postDigest . , :

 Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; 

, $digest , :

 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { this.$$postDigestQueue.shift()(); } }; 

, $$postDigest :

JS Bin
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { this.$$postDigestQueue.shift()(); } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); var postDigestInvoked = false; scope.$$postDigest(function() { postDigestInvoked = true; }); console.assert(!postDigestInvoked); scope.$digest(); console.assert(postDigestInvoked); 

:
 true
 true 



$scope Angular. . , .

Scope- Angular : watch-, $evalAsync $$postDigest — digest-. digest.

, try…catch
Angular $exceptionHandler. , .

Exception handling for $ evalAsync and $$ postDigest is done in the $ digest function . In both cases, the exception is logged, and the digest continues normally:

 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } }; 

The exception handling for the watch function is done in $ digestOnce :

 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; }; 

Now our digest loop is much safer to exceptions:

JS Bin code
View code
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); scope.aValue = "abc"; scope.counter = 0; scope.$watch(function() { throw "Watch fail"; }); scope.$watch( function(scope) { scope.$evalAsync(function(scope) { throw "async fail"; }); return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1); 

:
"Watch fail"
"async fail"
"Watch fail"
 true 


Disable the observer


, , , scope-, . - , , scope .

$watch Angular — , , . , , $watch , $$watchers :

 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; self.$$watchers.push(watcher); return function() { var index = self.$$watchers.indexOf(watcher); if (index >= 0) { self.$$watchers.splice(index, 1); } }; }; 

$watch , , :

JS Bin
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, valueEq: !!valueEq }; self.$$watchers.push(watcher); return function() { var index = self.$$watchers.indexOf(watcher); if (index >= 0) { self.$$watchers.splice(index, 1); } }; }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); scope.aValue = "abc"; scope.counter = 0; var removeWatch = scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1); scope.aValue = 'def'; scope.$digest(); console.assert(scope.counter === 2); removeWatch(); scope.aValue = 'ghi'; scope.$digest(); console.assert(scope.counter === 2); // No longer incrementing 

:
 true
 true
 true 


What's next


, scope-, Angular. scope- Angular — , , .

, scope Angular, . , scope- scope-, scope, , scope-. , — . (scope) .

, Scope .

From the translator:

The text is quite large, I think there are errors and typos. Send them in a personal - I'll fix everything.

If someone knows how to select lines in the code in Habré - say, this will improve the readability of the code.

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


All Articles