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));
true true true
function Scope() { }
var aScope = new Scope(); aScope.firstName = 'Jane'; aScope.lastName = 'Smith';
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.
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(); }); };
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();
"listener" "listener" "listener"
function(scope) { return scope.firstName; }
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; }); };
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);
true true true true
Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); };
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();
"digest listener fired" "digest listener fired" "digest listener fired"
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
true true false true
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); };
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.
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);
true true true
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);
0
In Angular TTL can be customized. We will come back to this in the next articles, when we discuss providers and dependency injection.
Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); };
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();
"Uncaught 10 digest iterations reached (line 36)"
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn, 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; };
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 : “ ”. , , , . . $watchCollection — .
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)); } };
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
Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); };
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
Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } };
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
function Scope() { this.$$watchers = []; this.$$asyncQueue = []; }
Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); };
scope (scope-), .
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); };
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"
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.$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.$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}); };
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"
function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; }
Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); };
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()(); } };
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
Angular $exceptionHandler. , .
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.$$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; };
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
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); } }; };
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
Source: https://habr.com/ru/post/201832/
All Articles