Create your own AngularJS-scope and Digest (4)
Chapter 1 Scope and Digest (4) Combination$apply
Call-$applyAsync
Call either in digest or outside$evalAsync
To delay work, he is actually designed for previous use cases. InsetTimeout
To call digest outside the digest Loop$evalAsync
To prevent confusion.
For external asynchronous calls in digest Loop$apply
Is also named$applyAsync
. Its usage is similar$apply
-To integrate code that is not aware of Angular digest loops. And$apply
The difference is that he does not calculate the given function immediately or initiate a digest immediately. Instead, the two tasks are scheduled to run in a short time.
Add$applyAsync
The original purpose of the function is to process the HTTP response. Every time$http
When the Service receives a response, any corresponding program is called and digest is called. This means that each HTTP request will have a digest running. For applications with a lot of HTTP traffic (for example, when many applications are started), there may be performance problems or digest loops that are costly. Now$http
Services available$applyAsync
In the same digest loop. However,$applyAsync
Not only try to solve$http
Service, you can also use it when the digest loop is used together.
As we can see in the first test case below, when we$applyAsync
A function is called after 50 milliseconds instead of being called immediately:
Test/scope_spec.js
it("allows async $apply with $applyAsync", function(done){ scope.counter = 0; scope.$watch( function(scope){ return scope.aValue; }, function(newValue, oldValue, scope){ scope.counter ++; } ); scope.$digest(); expect(scope.counter).toBe(1); scope.$applyAsync(function(scope){ scope.aValue = 'abc'; }); expect(scope.counter).toBe(1); setTimeout(function() { expect(scope.counter).toBe(2); done; }, 50);});
He and$evalAsync
There is no difference, but when we call$applyAsync
We start to see the difference. If we use$evalAsync
This function will be called in the same digest. However$applyAsync
AlwaysDeferred call:
Test/scope_spec.js
it("never executes $applyAsync'ed function in the same cycle", function(done){ scope.aValue = [1, 2, 3]; scope.asyncApplied = false; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope){ scope.$applyAsync(function(scope){ scope.asyncApplied = true; }); } ); scope.$digest(); expect(scope.asyncApplied).toBe(false); setTimeout(function(){ expect(scope.asyncApplied).toBe(true); done(); }, 50);});
Let's introduce another queue in the Scope constructor.$applyAsync
.
Src/scope. js
function Scope(){ this.$$watchers = [];this.$$lastDirtyWatch = null; this.$$asyncQueue = [];this.$$applyAsyncQueue = []; this.$$phase = null;}
When$applyAsync
We put this function into the queue. And$apply
Similarly, the function will calculate the given expression in the context of the current scope soon:
Src/scope. js
Scope.prototype.$applyAsync = function(expr){ var self = this; self.$$applyAsyncQueue.push(function(){ self.$eval(expr); });};
What we should do here is the scheduling function application. We can usesetTimeout
The delay is 0 milliseconds. In latency, we$apply
Retrieve each function from the queue and call all functions:
Src/scope. js
Scope.prototype.$applyAsync = function(expr){ var self = this; self.$$applyAsyncQueue.push(function(){ self.$eval(expr); }); setTimeout(function(){ self.$apply(function(){ while(self.$$applyAsyncQueue.length){self.$$applyAsyncQueue.shift()(); } }); }, 0);};
Note: We will not$apply
Each element in the queue. We are only outside the loop.$apply
Once. Here we only want one digest loop.
As we have discussed,$applyAsync
The most important thing is to optimize a series of quickly occurring tasks so that they can be completed in a digest. We have not completed this goal yet. Each call$applyAsync
A new digest will be scheduled. If we add a counter to the monitoring function, we can see this clearly:
Test/scope_spec.js
it("coalesces many calls to $applyAsync", function(done){ scope.counter = 0; scope.$watch( function(scope) { scope.counter ++; return scope.aValue; }, function(newValue, oldValue, scope){} ); scope.$applyAsync(function(scope){ scope.aValue = 'abc'; }); scope.$applyAsync(function(scope){ scope.aValue = 'def'; }); setTimeout(function() { expect(scope.counter).toBe(2); done(); }, 50);});
We want the counter value to be 2 (the monitoring is executed twice in the first digest), rather than 2.
What we need to do is to tracksetTimeout
Whether the process of traversing the queue is scheduled. We put this information on a private attribute of Scope, named$$applyAsyncId
:
Src/scope. js
function Scope(){ this.$$watchers = [];this.$$lastDirtyWatch = null; this.$$asyncQueue = [];this.$$applyAsyncQueue = []; this.$$applyAsyncId = null;this.$$phase = null;}
When we schedule a task, we first need to check this attribute and maintain its status during the task scheduling process until the end.
Src/scope. js
Scope.prototype.$applyAsync = function(expr){ var self = this; self.$$applyAsyncQueue.push(function(){ self.$eval(expr); }); if(self.$$applyAsyncId === null){self.$$applyAsyncId = setTimeout(function(){self.$apply(function(){while(self.$$applyAsyncQueue.length){self.$$applyAsyncQueue.shift()();}self.$$applyAsyncId = null; }); }, 0); }};
Note: Some people may not understand this solution. Note: When the input delay parameter in setTimeout is 0, functions in setTimeout will not be executed until the current setTimeout process is called. Two calls were made in the test case.$applyAsync
But setTimeout will not be executed until the setTimeout of the last row in the test case is executed, and then the setTimeout function is executed according to the delay in setTimeout. Because the second call$applyAsync
$ ApplyAsyncId is not empty, so a setTimeout is not set again. In the end, there are two settimeouts in this test case,$applyAsync
Will run first.
About$$applyAsyncId
On the other hand, if digest is initiated for some reason before the timeout is triggered, it should not initiate another digest. In this case, digest should traverse the queue and$applyAsync
Should be canceled:
Test/scope_spec.js
it('cancels and flushed $applyAsync if digested first', function(done){ scope.counter = 0; scope.$watch( function(scope) { scope.counter ++; return scope.aValue; }, function(newValue, oldValue, scope) {} ); scope.$applyAsync(function(scope){ scope.aValue = 'abc'; }); scope.$applyAsync(function(scope){ scope.aValue = 'def'; }); scope.$digest(); expect(scope.counter).toBe(2); expect(scope.aValue).toEqual('def'); setTimeout(function(){ expect(scope.counter).toBe(2); done(); }, 50);});
Here we tested if we call$digest
, Use$applyAsync
Each scheduled task is executed immediately. The task will not be left for future execution.
Let's extract$applyAsync
The internal function for clearing the queue, so that we can call it in many places:
Src/scope. js
Scope.prototype.$$flushApplyAsync = function() {while (this.$$applyAsyncQueue.length){this.$$applyAsyncQueue.shift()();}this.$$applyAsyncId = null;};
Src/scope. js
Scope.prototype.$applyAsync = function(expr){ var self = this; self.$$applyAsyncQueue.push(function(){ self.$eval(expr); }); if(self.$$applyAsyncId === null){self.$$applyAsyncId = setTimeout(function(){self.$apply(_.bind(self.$$flushApplyAsync, self)); }, 0); }};
LoDash _.bind
Functions and ECMAScript 5Function.prototype.bind
Equivalent function, used to determine the acceptance functionthis
Is a known value.
Now we can$digest
-If yes$applyAsync
Pending, we cancel it and immediately clear the task:
Src/scope. js
Scope.prototype.$digest = function(){ var tt1 = 10; var dirty; this.$$lastDirtyWatch = null; this.$beginPhase("$digest"); if(this.$$applyAsyncId){clearTimeout(this.$$applyAsyncId);this.$$flushApplyAsync(); } do { while (this.$$asyncQueue.length){var asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce(); if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {this.$clearPhase();throw '10 digest iterations reached';}} while (dirty || this.$$asyncQueue.length); this.$clearPhase();};
This is$applyAsync
All content. When you know that you have used it multiple times in a short period of time$apply
This is a slightly useful optimization.
Run the code after Digest-
$$postDigest
Another method is to add some code to the digest loop for running.$$postDigest
Function.
The two $ symbols before the function name mean the functions used internally by Angular, rather than the functions that developers can call. However, we also need to implement it here.
And$evalAsync
And$applyAsync
Similarly,$$postDigest
Run the scheduling function. Specifically, the function will run after the next digest. And$evalAsync
Similarly$$postDigest
The scheduled function runs only once. And$evalAsync
And$applyAsync
The difference is that$postDigest
Function andNoThis causes a digest to be scheduled, so the function is delayed until digest occurs for some reason. Below is a unit test that meets this requirement:
Test/scope_spec.js
it('runs a $$postDigest function after each digest', function(){ scope.counter = 0; scope.$$postDigest(function(){ scope.counter++; }); expect(scope.counter).toBe(0); scope.$digest(); expect(scope.counter).toBe(1); scope.$digest(); expect(scope.counter).toBe(1);});
As its name expresses,$postDigest
The function runs after digest, So if you use$postDigest
To change the scope. They will not be immediately noticed by the dirty checking mechanism. If you want to be noticed, you can manually call$digest
And or$apply
:
Test/scope_spec.js
it("doest not include $$postDigest in the digest", function(){ scope.aValue = 'original value'; scope.$$postDigest(function() { scope.aValue = 'changed value'; }); scope.$watch( function(scope){ return scope.aValue; }, function(newValue, oldValue, scope){ scope.watchedValue = newValue; } ); scope.$digest(); expect(scope.watchedValue).toBe("original value"); scope.$digest(); expect(scope.watchedValue).toBe("changed value");});
To achieve$postDigest
Let's initialize another array in the Scope constructor:
Src/scope. js
function Scope(){ this.$$watchers = [];this.$$lastDirtyWatch = null; this.$$asyncQueue = [];this.$$applyAsyncQueue = []; this.$$applyAsyncId = null;this.$$postDigestQueue = []; this.$$phase = null;}
Next, let's implement$postDigest
Itself. All he does is add the given function to the queue:
Src/scope. js
Scope.prototype.$$postDigest = function(fn){this.$$postDigestQueue.push(fn);};
Finally$digest
Let's retrieve the functions in the queue at a time and call them after digest is complete:
Src/scope. js
Scope.prototype.$digest = function(){ var tt1 = 10; var dirty; this.$$lastDirtyWatch = null; this.$beginPhase("$digest"); if(this.$$applyAsyncId){clearTimeout(this.$$applyAsyncId);this.$$flushApplyAsync(); } do { while (this.$$asyncQueue.length){var asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);}dirty = this.$$digestOnce(); if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {this.$clearPhase();throw '10 digest iterations reached';}} while (dirty || this.$$asyncQueue.length); this.$clearPhase(); while(this.$$postDigestQueue.length){this.$$postDigestQueue.shift()(); }};
We useArray.shift()
The method consumes the queue from the beginning of the queue until it is empty and runs these functions immediately.$postDigest
The function does not have any parameters.
Exception Handling
OurScope
Is becoming more and more like Angular. Then, he is vulnerable. This is mainly because we didn't put too much effort into exception handling.
If an exception occurs in a monitoring function$evalAsync
Or$applyAsync
Or$$postDigest
Function, and our current implementation will make an error and stop what he is doing. However, the implementation of Angular is more robust than ours. Before an exception is thrown or the exception captured by digest is recorded, and then the operation starts again.
Angular actually uses$exceptionHandler
To handle exceptions. Because we do not have this service yet, we only simply print the exception information on the console.
In watch, there are two possible exceptions: in the monitoring function and in the monitoring function. In either case, we want to print an exception and do nothing to execute the next watch. There are two test cases for these two cases:
Test/scope_spec.js
it("cathes exceptions in watch functions and continues", function(){ scope.aValue = 'abc'; scope.counter = 0; scope.$watch( function(scope) { throw "error"; }, function(newValue, oldValue, scope){ scope.counter ++; } ); scope.$watch( function(scope) { return scope.aValue;}, function(newValue, oldValue, scope) { scope.counter ++; } ); scope.$digest(); expect(scope.counter).toBe(1);});it("catches exceptions in listener functions and continues", function(){ scope.aValue = 'abc'; scope.counter = 0; scope.$watch( function(scope) { return scope.aValue;}, function(newValue, oldValue, scope){ throw "Error"; } ); scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter ++; } ); scope.$digest(); expect(scope.counter).toBe(1);});
In the above two cases, we have defined two monitors. The first one throws an exception. We checked whether the second monitor was executed.
To pass these two test cases, we need to modify them.$$digestOnce
Function, and usetry...catch
To encapsulate the execution of each monitoring function:
Src/scope. js
Scope.prototype.$$digestOnce = function(){var self = this;var newValue, oldValue, dirty;_.forEach(this.$$watchers, function(watcher){try{newValue = watcher.watchFn(self);oldValue = watcher.last;if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){ self.$$lastDirtyWatch = watcher; watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue; watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue: oldValue), self); dirty = true; }else if(self.$$lastDirtyWatch === watcher){ return false; } } catch (e){ console.error(e); } }); return dirty;};
$evalAsync
,$applyAsync
And$$postDigest
Exception Handling is also required. They are all used to execute arbitrary functions related to the digest loop. We don't want any of them to cause the loop to stop forever.
For$evalAsync
, We can define a test case to check whether$evalAsync
If an exception is thrown by any function scheduled, the monitoring function continues to run:
Test/scope_spec.js
it("catches exceptions in $evalAsync", function(done){ scope.aValue = 'abc'; scope.counter = 0; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope){ scope.counter ++; } ); scope.$evalAsync(function(scope){ throw "Error"; }); setTimeout(function(){ expect(scope.counter).toBe(1); done(); }, 50);});
For$applyAsync
, We define a test case to check that even if there is a function in$applyAsync
An exception is thrown before the scheduled function,$applyAsync
Can still be called:
Test/scope_spec.js
it("catches exceptions in $applyAsync", function(done) { scope.$applyAsync(function(scope) { throw "Error"; }); scope.$applyAsync(function(scope){ throw "Error"; }); scope.$applyAsync(function(scope){ scope.applied = true; }); setTimeout(function(){ expect(scope.applied).toBe(true); done(); }, 50);});
Here we use two functions that throw an exception. If we only use one function, the second function will actually run. This is because$apply
Called$digest
, In$apply
Offinally
Block$applyAsync
The queue has been consumed.
For$$postDigest
, Digest has been running, so there is no need to test it in the monitoring function. We can use the second$$postDigest
Make sure that it is executed as follows:
Test/scope_spec.js
it("catches exceptions in $$postDigest", function() { var didRun = false; scope.$$postDigest(function() {throw "Error";});scope.$$postDigest(function() { didRun = true; }); scope.$digest(); expect(didRun).toBe(true);});
For$evalAsync
And$$postDigest
Are included in$digest
Modifying the function. In both cases, we usetry...catch
Run the encapsulated function:
Src/scope. js
Scope.prototype.$digest = function(){ var tt1 = 10; var dirty; this.$$lastDirtyWatch = null; this.$beginPhase("$digest"); if(this.$$applyAsyncId){clearTimeout(this.$$applyAsyncId);this.$$flushApplyAsync(); } do { while (this.$$asyncQueue.length){try{var asyncTask = this.$$asyncQueue.shift();asyncTask.scope.$eval(asyncTask.expression);} catch(e){console.error(e);}}dirty = this.$$digestOnce(); if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {this.$clearPhase();throw '10 digest iterations reached';}} while (dirty || this.$$asyncQueue.length); this.$clearPhase(); while(this.$$postDigestQueue.length){try{this.$$postDigestQueue.shift()(); }catch (e){ console.error(e); } }};
Modify$applyAsync
On the other hand, that is, modifying the function for clearing the queue$$flushApplyAsync
:
Src/scope. js
Scope.prototype.$$flushApplyAsync = function() {while (this.$$applyAsyncQueue.length){try{this.$$applyAsyncQueue.shift()();}catch (e){console.error(e);}}this.$$applyAsyncId = null;};
When an exception occurs, our digest cycle is more robust than before.
Scan,