Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit a7cabe3

Browse files
committed
fix(WatchGroup): don't call reaction functions on removed WatchGroups
1 parent 8583d08 commit a7cabe3

File tree

5 files changed

+131
-10
lines changed

5 files changed

+131
-10
lines changed

lib/change_detection/watch_group.dart

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
9494
/// Pointer for creating tree of [WatchGroup]s.
9595
WatchGroup _watchGroupHead, _watchGroupTail, _previousWatchGroup,
9696
_nextWatchGroup;
97-
final WatchGroup _parentWatchGroup;
97+
WatchGroup _parentWatchGroup;
9898

9999
WatchGroup._child(_parentWatchGroup, this._changeDetector, this.context,
100100
this._cache, this._rootGroup)
@@ -115,6 +115,18 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
115115
_evalWatchTail = _evalWatchHead = _marker;
116116
}
117117

118+
get isAttached {
119+
var group = this;
120+
var root = _rootGroup;
121+
while(group != null) {
122+
if (group == root){
123+
return true;
124+
}
125+
group = group._parentWatchGroup;
126+
}
127+
return false;
128+
}
129+
118130
Watch watch(AST expression, ReactionFn reactionFn) {
119131
WatchRecord<_Handler> watchRecord =
120132
_cache.putIfAbsent(expression.expression,
@@ -268,6 +280,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
268280

269281
_WatchGroupList._remove(_parentWatchGroup, this);
270282
_changeDetector.remove();
283+
_rootGroup._removeCount++;
284+
_parentWatchGroup = null;
271285

272286
// Unlink the _watchRecord
273287
_EvalWatchRecord firstEvalWatch = _evalWatchHead;
@@ -318,6 +332,16 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
318332
class RootWatchGroup extends WatchGroup {
319333
Watch _dirtyWatchHead, _dirtyWatchTail;
320334

335+
/**
336+
* Every time a [WatchGroup] is destroyed we increment the counter. During
337+
* [detectChanges] we reset the count. Before calling the reaction function,
338+
* we check [_removeCount] and if it is unchanged we can safely call the
339+
* reaction function. If it is changed we only call the reaction function
340+
* if the [WatchGroup] is still attached.
341+
*/
342+
int _removeCount = 0;
343+
344+
321345
RootWatchGroup(ChangeDetector changeDetector, Object context):
322346
super._root(changeDetector, context);
323347

@@ -369,10 +393,14 @@ class RootWatchGroup extends WatchGroup {
369393
// We need to call reaction functions asynchronously. This processes the
370394
// asynchronous reaction function queue.
371395
Watch dirtyWatch = _dirtyWatchHead;
396+
RootWatchGroup root = _rootGroup;
397+
root._removeCount = 0;
372398
while(dirtyWatch != null) {
373399
count++;
374400
try {
375-
dirtyWatch.invoke();
401+
if (root._removeCount == 0 || dirtyWatch._watchGroup.isAttached) {
402+
dirtyWatch.invoke();
403+
}
376404
} catch (e, s) {
377405
if (exceptionHandler == null) rethrow; else exceptionHandler(e, s);
378406
}
@@ -408,15 +436,15 @@ class Watch {
408436

409437
final Record<_Handler> _record;
410438
final ReactionFn reactionFn;
439+
final WatchGroup _watchGroup;
411440

412441
bool _dirty = false;
413442
bool _deleted = false;
414443
Watch _nextDirtyWatch;
415444

416-
Watch(this._record, this.reactionFn);
445+
Watch(this._watchGroup, this._record, this.reactionFn);
417446

418447
get expression => _record.handler.expression;
419-
420448
void invoke() {
421449
if (_deleted || !_dirty) return;
422450
_dirty = false;
@@ -468,7 +496,7 @@ abstract class _Handler implements _LinkedList, _LinkedListItem, _WatchList {
468496
Watch addReactionFn(ReactionFn reactionFn) {
469497
assert(_next != this); // verify we are not detached
470498
return watchGrp._rootGroup._addDirtyWatch(_WatchList._add(this,
471-
new Watch(watchRecord, reactionFn)));
499+
new Watch(watchGrp, watchRecord, reactionFn)));
472500
}
473501

474502
void addForwardHandler(_Handler forwardToHandler) {

lib/core/scope.dart

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,27 @@ class Scope {
150150
* The parent [Scope].
151151
*/
152152
Scope get parentScope => _parentScope;
153-
bool get isAttached => _parentScope == null ? false : _parentScope.isAttached;
153+
154+
/**
155+
* Return `true` if the scope has been destroyed. Once scope is destroyed
156+
* No operations are allowed on it.
157+
*/
158+
bool get isDestroyed {
159+
var scope = this;
160+
var root = rootScope;
161+
while(scope != null) {
162+
if (scope == root) {
163+
return false;
164+
}
165+
scope = scope._parentScope;
166+
}
167+
return true;
168+
}
169+
170+
/**
171+
* Returns true if the scope is still attached to the [RootScope].
172+
*/
173+
bool get isAttached => !isDestroyed;
154174

155175
// TODO(misko): WatchGroup should be private.
156176
// Instead we should expose performance stats about the watches
@@ -204,6 +224,7 @@ class Scope {
204224
}
205225

206226
dynamic eval(expression, [Map locals]) {
227+
assert(isAttached);
207228
_assertInternalStateConsistency();
208229
assert(expression == null ||
209230
expression is String ||
@@ -236,19 +257,23 @@ class Scope {
236257
}
237258

238259
ScopeEvent emit(String name, [data]) {
260+
assert(isAttached);
239261
_assertInternalStateConsistency();
240262
return _Streams.emit(this, name, data);
241263
}
242264
ScopeEvent broadcast(String name, [data]) {
265+
assert(isAttached);
243266
_assertInternalStateConsistency();
244267
return _Streams.broadcast(this, name, data);
245268
}
246269
ScopeStream on(String name) {
270+
assert(isAttached);
247271
_assertInternalStateConsistency();
248272
return _Streams.on(this, rootScope._exceptionHandler, name);
249273
}
250274

251275
Scope createChild(Object childContext) {
276+
assert(isAttached);
252277
_assertInternalStateConsistency();
253278
var child = new Scope(childContext, rootScope, this,
254279
_readWriteGroup.newGroup(childContext),
@@ -263,6 +288,7 @@ class Scope {
263288
}
264289

265290
void destroy() {
291+
assert(isAttached);
266292
_assertInternalStateConsistency();
267293
broadcast(ScopeEvent.DESTROY);
268294
_Streams.destroy(this);
@@ -359,10 +385,7 @@ class RootScope extends Scope {
359385
new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context),
360386
new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context))
361387
{
362-
_zone.onTurnDone = () {
363-
digest();
364-
flush();
365-
};
388+
_zone.onTurnDone = apply;
366389
}
367390

368391
RootScope get rootScope => this;

test/change_detection/watch_group_spec.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,28 @@ main() => describe('WatchGroup', () {
597597
});
598598

599599

600+
it('should not call reaction function on removed group', () {
601+
var log = [];
602+
context['name'] = 'misko';
603+
var child = watchGrp.newGroup(context);
604+
watchGrp.watch(parse('name'), (v, _) {
605+
log.add('root $v');
606+
if (v == 'destroy') {
607+
child.remove();
608+
}
609+
});
610+
child.watch(parse('name'), (v, _) => log.add('child $v'));
611+
watchGrp.detectChanges();
612+
expect(log).toEqual(['root misko', 'child misko']);
613+
log.clear();
614+
615+
context['name'] = 'destroy';
616+
watchGrp.detectChanges();
617+
expect(log).toEqual(['root destroy']);
618+
});
619+
620+
621+
600622
it('should watch children', () {
601623
var childContext = new PrototypeMap(context);
602624
context['a'] = 'OK';

test/core/scope_spec.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,27 @@ main() => describe('scope', () {
556556
first.destroy();
557557
expect(log).toEqual(['first', 'first-child']);
558558
}));
559+
560+
561+
it('should not call reaction function on destroyed scope', inject((RootScope rootScope, Logger log) {
562+
rootScope.context['name'] = 'misko';
563+
var child = rootScope.createChild(rootScope.context);
564+
rootScope.watch('name', (v, _) {
565+
log('root $v');
566+
if (v == 'destroy') {
567+
child.destroy();
568+
}
569+
});
570+
rootScope.watch('name', (v, _) => log('root2 $v'));
571+
child.watch('name', (v, _) => log('child $v'));
572+
rootScope.apply();
573+
expect(log).toEqual(['root misko', 'root2 misko', 'child misko']);
574+
log.clear();
575+
576+
rootScope.context['name'] = 'destroy';
577+
rootScope.apply();
578+
expect(log).toEqual(['root destroy', 'root2 destroy']);
579+
}));
559580
});
560581

561582

test/directive/ng_if_spec.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,31 @@ main() {
195195
expect(logger.result()).toEqual('ALWAYS; JAMES');
196196
}
197197
);
198+
199+
they('should prevent other directives from running when disabled',
200+
[
201+
'<div><div ng-if="a"><div ng-if="b">content</div></div></div>',
202+
'<div><div ng-unless="!a"><div ng-unless="!b">content</div></div></div>'],
203+
(html) {
204+
compile(html);
205+
expect(element.find('span').html()).toEqual('');
206+
207+
expect(() {
208+
rootScope.apply(() {
209+
rootScope.context['a'] = true;
210+
rootScope.context['b'] = false;
211+
});
212+
}).not.toThrow();
213+
expect(element.find('span').html()).toEqual('');
214+
215+
216+
expect(() {
217+
rootScope.apply(() {
218+
rootScope.context['a'] = false;
219+
rootScope.context['b'] = true;
220+
});
221+
}).not.toThrow();
222+
expect(element.find('span').html()).toEqual('');
223+
}
224+
);
198225
}

0 commit comments

Comments
 (0)