Create your own AngularJS-scope and Digest (4)

Source: Internet
Author: User

Create your own AngularJS-scope and Digest (4)
Chapter 1 Scope and Digest (4) Combination$applyCall-$applyAsync

Call either in digest or outside$evalAsyncTo delay work, he is actually designed for previous use cases. InsetTimeoutTo call digest outside the digest Loop$evalAsyncTo prevent confusion.

For external asynchronous calls in digest Loop$applyIs also named$applyAsync. Its usage is similar$apply-To integrate code that is not aware of Angular digest loops. And$applyThe 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$applyAsyncThe original purpose of the function is to process the HTTP response. Every time$httpWhen 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$httpServices available$applyAsyncIn the same digest loop. However,$applyAsyncNot only try to solve$httpService, you can also use it when the digest loop is used together.

As we can see in the first test case below, when we$applyAsyncA 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$evalAsyncThere is no difference, but when we call$applyAsyncWe start to see the difference. If we use$evalAsyncThis function will be called in the same digest. However$applyAsyncAlwaysDeferred 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$applyAsyncWe put this function into the queue. And$applySimilarly, 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 usesetTimeoutThe delay is 0 milliseconds. In latency, we$applyRetrieve 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$applyEach element in the queue. We are only outside the loop.$applyOnce. Here we only want one digest loop.

As we have discussed,$applyAsyncThe 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$applyAsyncA 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 tracksetTimeoutWhether 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.$applyAsyncBut 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,$applyAsyncWill run first.

About$$applyAsyncIdOn 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$applyAsyncShould 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$applyAsyncEach scheduled task is executed immediately. The task will not be left for future execution.

Let's extract$applyAsyncThe 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 _.bindFunctions and ECMAScript 5Function.prototype.bindEquivalent function, used to determine the acceptance functionthisIs a known value.

Now we can$digest-If yes$applyAsyncPending, 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$applyAsyncAll content. When you know that you have used it multiple times in a short period of time$applyThis is a slightly useful optimization.

Run the code after Digest- $$postDigest

Another method is to add some code to the digest loop for running.$$postDigestFunction.

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$evalAsyncAnd$applyAsyncSimilarly,$$postDigestRun the scheduling function. Specifically, the function will run after the next digest. And$evalAsyncSimilarly$$postDigestThe scheduled function runs only once. And$evalAsyncAnd$applyAsyncThe difference is that$postDigestFunction 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,$postDigestThe function runs after digest, So if you use$postDigestTo change the scope. They will not be immediately noticed by the dirty checking mechanism. If you want to be noticed, you can manually call$digestAnd 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$postDigestLet'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$postDigestItself. All he does is add the given function to the queue:

Src/scope. js

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

Finally$digestLet'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.$postDigestThe function does not have any parameters.

Exception Handling

OurScopeIs 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$evalAsyncOr$applyAsyncOr$$postDigestFunction, 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$exceptionHandlerTo 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.$$digestOnceFunction, and usetry...catchTo 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,$applyAsyncAnd$$postDigestException 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$evalAsyncIf 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$applyAsyncAn exception is thrown before the scheduled function,$applyAsyncCan 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$applyCalled$digest, In$applyOffinallyBlock$applyAsyncThe 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$$postDigestMake 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$evalAsyncAnd$$postDigestAre included in$digestModifying the function. In both cases, we usetry...catchRun 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$applyAsyncOn 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,

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.