Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to build Scope and Digest in AngularJS

2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article introduces the knowledge of "how to build Scope and Digest in AngularJS". Many people will encounter this dilemma in the operation of actual cases, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!

Scope object

Angular's Scope objects are POJO (simple JavaScript objects) on which properties can be added just like any other object. The Scope object is created with a constructor, so let's write the simplest version:

Function Scope () {}

Now we can use the new operator to create a Scope object. We can also attach some attributes to it:

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

There is nothing special about these attributes. There is no need to call a special setter, and there are no restrictions when assigning values. Instead, something wonderful happened in two special functions: $watch and $digest.

Monitoring object properties: $watch and $digest

$watch and $digest complement each other. Together, the two constitute the core of the Angular scope: the response to data changes.

With $watch, you can add a listener to the Scope. The listener is prompted when a change occurs on the Scope. Specify the following two functions to $watch to create a listener:

A monitor function that specifies which part of the data you are interested in.

A listening function that is used to accept prompts when data changes.

As an Angular user, generally speaking, you monitor an expression instead of using a monitoring function. A monitor expression is a string, such as "user.firstName", usually specified in a data binding, an attribute of an instruction, or JavaScript code, which is parsed and compiled into a monitor function by Angular. We will discuss how this is done later in this article. In this article, we will use a slightly lower-level approach to provide monitoring capabilities directly.

To implement $watch, we need to store all registered listeners. We add an array to the Scope constructor:

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

In the Angular framework, the double dollar prefix $$means that this variable is considered private and should not be called in external code.

Now we can define the $watch method. It takes two functions as arguments and stores them in the $$watchers array. We need to store these functions on each Scope instance, so put it on the prototype of Scope:

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

The other side is the $digest function. It executes all listeners registered in scope. Let's implement a simplified version of it, traversing all listeners and calling their listeners:

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

Now we can add the listener and run $digest, which will call the listener function:

Http://jsbin.com/oMaQoxa/2/embed?js,console

These in themselves are not very useful, what we want is to be able to detect whether the value specified by the monitoring function has actually changed, and then call the listening function.

Dirty value detection

As mentioned above, the listener's listener function should return a change in the part of the data we are concerned about, which usually exists in the scope. To make it easier to access the scope, use the current scope as the argument when calling the monitoring function. A listener that focuses on the fiestName attribute on the scope looks like this:

Function (scope) {return scope.firstName;}

This is the general form of a monitoring function: get some values from the scope and return them.

The purpose of the $digest function is to call the monitor function and compare the difference between the value it returned and the last one. If not, the listener is dirty and its listener function should be called.

To do this, $digest needs to remember the last value returned by each monitor function. Now that we have created an object for each listener, just store the previous value on it. Here is a new implementation of $digest to detect changes in the value of each monitoring function:

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 listener, we call the monitor function, pass the scope itself as an argument, and then compare this return value with the last return value, and if it is different, call the listener function. For convenience, we pass both old and new values and scopes as parameters to the listener function. Finally, we set the listener's last property to the newly returned value, which we can use for comparison next time.

With this implementation in place, we can see how the listener function executes when $digest is called:

Http://jsbin.com/OsITIZu/3/embed?js,console

We have implemented the essence of Angular scope: add listeners and run them in digest.

You can also see several important performance features about the Angular scope:

Adding data to a scope does not in itself provide a performance discount. If there is no listener monitoring a property, it doesn't matter whether it is in scope or not. Angular does not traverse the properties of the scope, it traverses the listener.

Each monitoring function is called in $digest, so * focuses on the number of listeners and the performance of each independent monitoring function or expression.

Get a hint on Digest

If you want to be notified every time the scope of an Angular is digest, you can take advantage of executing listeners one by one every time you digest. Just register a listener that does not have a listener function.

To support this use case, we need to check in $watch to see if the monitor function is omitted, and if so, replace it with an empty function:

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

If you use this pattern, keep in mind that even if there is no listenerFn,Angular, you will look for the return value of watchFn. If a value is returned, the value is submitted to the dirty check. If you want to use this usage and avoid unnecessary things, as long as the monitor function does not return any value. In this example, the value of the listener is always undefined.

Http://jsbin.com/OsITIZu/4/embed?js,console

This is the core of this implementation, but it is still far from the final one. For example, there is a typical scenario that we cannot support: the listener function itself also modifies the properties on the scope. If this happens, another listener is monitoring the modified property, and the change may not be detected in the same digest:

Http://jsbin.com/eTIpUyE/2/embed?js,console

Let's fix this problem.

Persistent Digest when data is dirty

We need to change the digest so that it continues to traverse all listeners until the monitored value stops changing.

First, we rename the current $digest function to $$digestOnce, which runs all listeners once and returns a Boolean value indicating whether there are any changes:

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;}

Then, we redefine $digest, which runs as an "outer loop", calling $$digestOnce when a change occurs:

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

Digest now runs each listener at least once. If the monitoring value is changed after running for * times, it is marked as dirty, and all listeners will run again. This will run until all the monitored values no longer change and the whole situation stabilizes.

There is not really a function in the Angular scope called $$digestOnce. On the contrary, the digest loop is contained in $digest. Our goal is more clarity than performance, so we encapsulate the inner loop as a function.

Here is the new implementation:

Http://jsbin.com/Imoyosa/3/embed?js,console

We can now have another important understanding of Angular listeners: they can be executed multiple times in a single digest. This is why it is often said that listeners should be idempotent: a listener should have no boundary effect, or the boundary effect should occur only a limited number of times. For example, suppose a monitor function triggers an Ajax request, and you can't determine how many requests your application has sent.

In our current implementation, there is an obvious omission: what if two listeners monitor each other's changes? In other words, what if the state is never stable? This situation is shown in the following code. In this example, the $digest call is commented out, uncomment and see what happens:

Http://jsbin.com/eKEvOYa/3/embed?js,console

JSBin stopped after running for a while (about 100000 times on my machine). If you run in something else, such as a Node.js, it will run forever.

Give up unstable digest

What we need to do is to keep the digest running within an acceptable number of iterations. If the scope is still changing after so many times, let go and announce that it will never be stable. At this point, we will throw an exception, because no matter what the state of the scope becomes, it is unlikely to be the result the user wants.

The iterative * * value is called TTL (short for Time To Live). This value defaults to 10, which may be a little small (we just ran this digest 100000 times! But keep in mind that this is a performance-sensitive place because digest is often executed and each digest runs all listeners Users are also unlikely to create more than 10 chain listeners.

In fact, the TTL in Angular can be adjusted. We will review this topic in future articles when we discuss provider and dependency injection.

Let's go ahead and add a loop counter to the outer digest loop. If the TTL is reached, an exception is thrown:

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

The following is the updated version, which allows the monitoring example referenced in a loop to throw an exception:

Http://jsbin.com/uNapUWe/2/embed?js,console

These things should have been made clear about digest.

Now, let's turn our attention to how to detect changes.

Value-based dirty check

We have used the strict equal operator (=) to compare the new and old values, in most cases, it is good, such as all the basic types (numbers, strings, etc.), you can also detect whether an object or array has become new, but Angular also has a way to detect changes when changes occur within the object or array. That is: you can monitor changes in values, not references.

This kind of dirty check needs to be turned on as a flag by passing the third optional parameter of Boolean type to the $watch function. When this flag is true, value-based checking is turned on. Let's redefine $watch, accept this parameter, and store it in the listener:

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

All we do is add this flag to the listener and cast it to a Boolean type twice. When the user calls $watch without passing in the third parameter, the valueEq is undefined and becomes false in the listener object.

Value-based dirty checking means that if the old and new values are objects or arrays, we have to traverse everything contained in them. If there is any difference between them, the listener will be dirty. If the value contains nested objects or arrays, it will also be compared recursively by value.

Angular has its own equality detection function built in, but we will use the one provided by Lo-Dash. Let's define a new function, take two values and a Boolean flag, and compare the corresponding values:

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

In order to prompt for a change in the value, we also need to change the way we previously stored the old value on each listener. It is not enough to store only references to the current value, because changes that occur within this value will also take effect on its references. The $$areEqual method compares two references of the same value to always be true, so we need to make a deep copy of the current value and store them.

Just like equality detection, Angular also has its own deep copy function built in, but we still use Lo-Dash to provide it. Let's modify $digestOnce, use the new $$areEqual function internally, and copy the reference once, if necessary:

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 we can see the difference between the two dirty detection methods:

Http://jsbin.com/ARiWENO/3/embed?js,console

The way to check values is obviously a more complex operation than checking references. Traversing nested data structures takes time, and maintaining deep copies of data takes up a lot of memory. This is why Angular does not use value-based dirty detection by default, and users need to explicitly set this flag to open it.

Angular also provides a third method of dirty detection: collective monitoring. Just like value-based detection, it can also prompt for changes in objects and arrays. But different from the value-based detection, it does a relatively shallow detection and does not go into the deep layer recursively, so it is more efficient than the value-based detection. Collection detection is used through the "$watchCollection" function, and we'll see how it is implemented later in this series.

Before we finish the comparison of values, there are some strange JavaScript things to deal with.

Non-digital (NaN)

In JavaScript, NaN (Not-a-Number) is not equal to itself, which sounds strange, but it is. If we do not explicitly handle NaN in the dirty detection function, a listener with a value of NaN will always be dirty.

For value-based dirty detection, this has been handled by Lo-Dash 's isEqual function. For reference-based dirty detection, we need to handle it ourselves. To modify the code of the $$areEqual function:

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));}}

Now the listener with NaN is working:

Http://jsbin.com/ijINaRA/2/embed?js,console

Now that value-based detection is implemented, it's time to focus on how the application code deals with scope.

$eval-executes code on the context of the scope

In Angular, there are several ways to execute code in the context of a scope, the simplest of which is $eval. It takes a function as an argument, and all it does is execute the incoming function immediately, pass the scope itself to it as an argument, and return the return value of the function. $eval can also have a second argument, and all it does is pass it to the function.

The implementation of $eval is simple:

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

The use of $eval is just as simple:

Http://jsbin.com/UzaWUC/1/embed?js,console

So why execute a function in such an obviously redundant way? Some people think that some code deals specifically with the content of the scope, and $eval makes it all the more obvious. $scope is also part of building $apply, which we'll talk about later.

Then, perhaps the most interesting use of $eval is when we don't pass in functions, but expressions. Just like $watch, you can give $eval a string expression that compiles the expression and executes it in the context of the scope. We will implement this later in the series.

$apply-integrates external code with digest loops

Perhaps the most famous of all the functions on Scope is $apply. There is a good reason why it is hailed as the most standard way to integrate external libraries into Angular.

$apply takes a function as an argument, it executes the function with $eval, and then triggers the digest loop with $digest. Here is a simple implementation:

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

The call to $digest is placed in the finally block to ensure that digest is executed even if the function throws an exception.

The big idea about $apply is that we can execute code that has nothing to do with Angular, and that code can still change things in scope, and $apply ensures that scoped listeners can detect these changes. When people talk about using $apply to integrate code into the "Angular lifecycle," they mean this, and nothing is more important.

Here is the practice of $apply:

Http://jsbin.com/UzaWUC/2/embed?js,console

Deferred execution-$evalAsync

In JavaScript, it is common to "delay" the execution of a piece of code-delaying its execution to a future point after the end of the current execution context. The most common way is to call the setTimeout () function, passing a 0 (or very small) as the delay parameter.

This pattern also applies to Angular programs, but it is more recommended to use $timeout services and use $apply to integrate functions to be deferred into the digest lifecycle.

But there is another way to delay code in Angular, and that is the $evalAsync function on Scope. $evalAsync accepts a function, plans it, and executes it in the current ongoing digest or before the next digest. For example, you can delay the execution of some code in the listener function of a listener, even if it has been delayed, it will still be executed in the existing digest traversal.

The first thing we need is to store the scheduled task of $evalAsync, which can be done by initializing an array in the Scope constructor:

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

Let's define $evalAsync, which adds the functions that will be executed on this queue:

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

We explicitly set the current scope on queued objects in order to use scope inheritance, which we will discuss in the next article in this series.

Then, the * thing we need to do in $digest is to take everything out of the queue and use $eval to trigger all delayed functions:

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);}

This implementation ensures that if you want to delay the execution of a function if the scope is dirty, the function will be executed later, but still in the same digest.

Here is an example of how to use $evalAsync:

Http://jsbin.com/ilepOwI/1/embed?js,console

Scope phase

The other thing $evalAsync does is to delay execution of a given $digest if there is no other $digest running right now. This means that whenever you call $evalAsync, you can be sure that the function you want to delay execution will be executed "very quickly" rather than waiting for something else to trigger a digest.

There needs to be a mechanism for $evalAsync to detect whether a $digest is already running because it does not want to affect the one that is scheduled to be executed. For this reason, the scope of Angular implements something called phase, which is a simple string property in the scope that stores the information you are doing now.

In the constructor of Scope, we introduce a field called $$phase, which is initialized to null:

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

Then, we define some methods to control this phase variable: one for setting, one for clearing, and an extra check to ensure that the phase that has already been activated is not set again:

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

In the $digest method, let's set the phase property to "$digest" from the outer loop:

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 ();}

Let's modify the $apply and set the same stage as ourselves in it. This can be useful when debugging:

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

Finally, the scheduling of $digest is put into $evalAsync. It detects the existing phase variables in the scope, and if there are no (and no scheduled asynchronous tasks), the digest is scheduled.

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});}

With this implementation, no matter when and where you call $evalAsync, you can be sure that a digest will happen in the near future.

Http://jsbin.com/iKeSaGi/1/embed?js,console

Execute code after digest-$$postDigest

Another way to attach code to the digest loop is to include a $$postDigest function in the plan.

In Angular, a double dollar sign before the function name indicates that it is an internal thing that should not be used by application developers. But it does exist, so we have to realize it, too.

Just like $evalAsync, $$postDigest can plan a function to run "later". Specifically, this function will be run after the next digest completes. Including a $$postDigest function in the plan will not cause a digest to be delayed, so the execution of this function will be delayed until a digest is caused by some other reason. As the name implies, the $$postDigest function runs after digest, and if you change the scope in $$digest, you need to manually call $digest or $apply to ensure that these changes take effect.

First, we queue the constructor of Scope, which is used by the $$postDigest function:

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

Then, we add $$postDigest, and what it does is add the given function to the queue:

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

Finally, in $digest, when the digest is complete, all the functions in the queue are executed.

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 () ();}}

Here is how to use the $$postDigest function:

Http://jsbin.com/IMEhowO/1/embed?js,console

Exception handling

The existing implementation of Scope is getting closer to what it actually looks like in Angular, but it's still a little fragile because we haven't put much effort into exception handling so far.

The scope of Angular is very robust when it encounters an error: when an exception is generated, digest is not terminated in the monitor function, in the $evalAsync function, or in the $$postDigest function. In our current implementation, making an exception in any of these places will hang up the entire $digest.

We can easily fix it and package the above three calls in try. It would be nice to be in catch.

Angular actually throws these exceptions to its $exceptionHandler service. Since we don't have it yet, let's throw it on the console.

Exception handling for $evalAsync and $$postDigest is in the $digest function. In these scenarios, exceptions thrown from planned programs are logged, and the exceptions that follow are still running:

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 listener is placed 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 more robust when it encounters an exception.

Http://jsbin.com/IMEhowO/2/embed?js,console

Destroy a listener

When registering a listener, it is generally necessary to keep it throughout the life cycle of the scope, so it is rarely explicitly removed. There are also scenarios where the scope needs to be maintained but a listener needs to be removed.

The $watch function in Angular has a return value, which is a function that, if executed, destroys the listener you just registered. To do this in our version, simply return a function that removes the monitor from the $$watchers array:

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);}};}

We can now save the return value of $watch and call it later to remove the listener:

Http://jsbin.com/IMEhowO/4/embed?js,console

That's all for "how to build Scope and Digest in AngularJS". Thank you for reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report