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

Commit 9387456

Browse files
EisenbergEffectcaitp
authored andcommitted
feat(watch_group): split group manipulation into newGroup and addGroup
Previously, creating a new watch group simultaneously attached it to the parent from which it was created. Now, we have split this behavior into two steps so that it is possible to create a watch group, set up watches and then choose which parent to attach it to at a later point in time. You can also remove a watch group and attach it to a different parent. These features allow a more elegant mechanism for turning watches on/off based on whether or not the associated view is attached to the DOM. A primary use case for this is to tie into the attachedCallback/detachedCallback of the custom element lifecycle in order to better manage databinding present in element templates. Closes issue #32
1 parent 6364b5a commit 9387456

File tree

4 files changed

+167
-41
lines changed

4 files changed

+167
-41
lines changed

src/watch_group.js

+39-27
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,14 @@ function putIfAbsent(obj, key, ctor) {
3232
}
3333

3434
export class WatchGroup {
35-
constructor(parentWatchGroup, getterCache, context, cache, rootGroup) {
36-
// TODO: Traceur Assertions
37-
// assert(parentWatchGroup is WatchGroup)
38-
// assert(changeDetector is ChangeDetector)
39-
// assert(context and context is Function or Object)
40-
// assert(rootGroup is RootWatchGroup)
41-
this._parentWatchGroup = parentWatchGroup;
35+
constructor(id, getterCache, context, cache, rootGroup) {
36+
this.id = id;
37+
4238
// Initialize _WatchGroupList
4339
this._watchGroupHead = this._watchGroupTail = null;
4440
this._nextWatchGroup = this._prevWatchGroup = null;
45-
this.id = `${parentWatchGroup.id}.${parentWatchGroup._nextChildId++}`;
41+
this._dirtyWatchHead = this._dirtyWatchTail = null;
42+
4643
this._getterCache = getterCache;
4744
this.context = context;
4845
this._cache = cache;
@@ -54,8 +51,7 @@ export class WatchGroup {
5451
this._evalWatchHead = this._evalWatchTail = this._marker;
5552

5653
this._dirtyMarker = ChangeRecord.marker();
57-
this._recordTail = this._parentWatchGroup._childInclRecordTail;
58-
this._recordHead = this._recordTail = this._recordAdd(this._dirtyMarker);
54+
this._recordHead = this._recordTail = this._dirtyMarker;
5955

6056
// Stats...
6157
this.fieldCost = 0; // Stats: Number of field watchers which are in use
@@ -179,51 +175,66 @@ export class WatchGroup {
179175
// [context]. If not present than child expressions will evaluate on same context allowing
180176
// the reuse of the expression cache.
181177
newGroup(context) {
182-
var prev = this._childWatchGroupTail._evalWatchTail;
183-
var next = prev._nextEvalWatch;
184-
185178
if (arguments.length === 0 || context === null) {
186179
context = this.context;
187180
}
188181

189182
var root = this._rootGroup === null ? this : this._rootGroup;
183+
var id = `${this.id}.${this._nextChildId++}`;
190184
var cache = context === null ? this._cache : {};
191185

192-
var childGroup = new WatchGroup(this, this._getterCache, context, cache, root);
186+
var childGroup = new WatchGroup(id, this._getterCache, context, cache, root);
187+
188+
return childGroup;
189+
}
190+
191+
addGroup(childGroup){
192+
childGroup.id = `${this.id}.${this._nextChildId++}`;
193+
childGroup._parentWatchGroup = this;
194+
195+
var prevEval = this._childWatchGroupTail._evalWatchTail;
196+
var nextEval = prevEval._nextEvalWatch;
197+
var prevRecord = this._childWatchGroupTail._recordTail;
198+
var nextRecord = prevRecord.nextRecord;
199+
193200
_WatchGroupList._add(this, childGroup);
194201

195-
var marker = childGroup._marker;
202+
var evalMarker = childGroup._marker;
196203

197-
marker._prevEvalWatch = prev;
198-
marker._nextEvalWatch = next;
204+
evalMarker._prevEvalWatch = prevEval;
205+
evalMarker._nextEvalWatch = nextEval;
199206

200-
if (prev !== null) prev._nextEvalWatch = marker;
201-
if (next !== null) next._prevEvalWatch = marker;
207+
if (prevEval !== null) prevEval._nextEvalWatch = evalMarker;
208+
if (nextEval !== null) nextEval._prevEvalWatch = evalMarker;
202209

203-
return childGroup;
210+
var childRecordHead = childGroup._recordHead;
211+
var childRecordTail = childGroup._recordTail;
212+
213+
childRecordHead.prevRecord = prevRecord;
214+
childRecordTail.nextRecord = nextRecord;
215+
216+
if (prevRecord !== null) prevRecord.nextRecord = childRecordHead;
217+
if (nextRecord !== null) nextRecord.prevRecord = childRecordTail;
218+
219+
// TODO:(eisenbergeffect) attach observers associated with dirty records?
204220
}
205221

206-
// Remove/destroy [WatchGroup] and all of its watches
207222
remove() {
208-
// TODO:(misko) This code is not right.
209-
// 1) It fails to release [ChangeDetector] [WatchRecord]s
223+
// TODO:(eisenbergeffect) detach observers associated with dirty records?
210224

211225
var prevRecord = this._recordHead.prevRecord;
212226
var nextRecord = this._childInclRecordTail.nextRecord;
213227

214228
if (prevRecord !== null) prevRecord.nextRecord = nextRecord;
215229
if (nextRecord !== null) nextRecord.prevRecord = prevRecord;
216230

231+
// TODO:(eisenbergeffect) investigate these two lines; not sure such properties exist on records
217232
this._recordHead._prevWatchGroup = null;
218233
this._recordTail._prevWatchGroup = null;
219-
this._recordHead = this._recordTail = null;
220-
221234

222235
_WatchGroupList._remove(this._parentWatchGroup, this);
223236
this._nextWatchGroup = this._prevWatchGroup = null;
224237

225-
//TODO: this._changeDetector.remove();
226-
227238
this._rootGroup._removeCount++;
228239
this._parentWatchGroup = null;
229240

@@ -616,3 +627,4 @@ export class RootWatchGroup extends WatchGroup {
616627
return watch;
617628
}
618629
}
630+

test/dirtychecking.spec.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ describe('DirtyCheckingChangeDetector', function() {
132132
setup();
133133
var obj = {};
134134
var ra = watchGrp.watchField(obj, 'a', '0a');
135-
var child1a = watchGrp.newGroup();
136-
var child1b = watchGrp.newGroup();
137-
var child2 = child1a.newGroup();
135+
var child1a = createAndAddGroup(watchGrp);
136+
var child1b = createAndAddGroup(watchGrp);
137+
var child2 = createAndAddGroup(child1a);
138138

139139
child1a.watchField(obj, 'a', '1a');
140140
child1b.watchField(obj, 'a', '1b');
@@ -153,7 +153,7 @@ describe('DirtyCheckingChangeDetector', function() {
153153
setup();
154154
var obj = {};
155155
var ra = watchGrp.watchField(obj, 'a', 'a');
156-
var child = watchGrp.newGroup();
156+
var child = createAndAddGroup(watchGrp);
157157
var cb = child.watchField(obj, 'b', 'b');
158158

159159
obj['a'] = obj['b'] = 1;
@@ -177,9 +177,9 @@ describe('DirtyCheckingChangeDetector', function() {
177177

178178
it('should properly add children', function() {
179179
setup();
180-
var a = watchGrp.newGroup();
181-
var aChild = a.newGroup();
182-
var b = watchGrp.newGroup();
180+
var a = createAndAddGroup(watchGrp);
181+
var aChild = createAndAddGroup(a);
182+
var b = createAndAddGroup(watchGrp);
183183
expect(function() {
184184
watchGrp.collectChanges();
185185
}).not.toThrow();
@@ -580,3 +580,10 @@ class _User {
580580
this.age = age;
581581
}
582582
}
583+
584+
function createAndAddGroup(parentGroup, context){
585+
var childGroup = parentGroup.newGroup(context);
586+
parentGroup.addGroup(childGroup);
587+
return childGroup;
588+
}
589+

test/observer.spec.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe('observer', function() {
123123
group;
124124

125125
setup(observer);
126-
group = watchGrp.newGroup();
126+
group = createAndAddGroup(watchGrp);
127127
group.watchField(user, 'name', null);
128128

129129
expect(observer.closeCalls).toBe(0);
@@ -183,3 +183,10 @@ class ExplicitObserver{
183183
this.callback = null;
184184
}
185185
}
186+
187+
function createAndAddGroup(parentGroup, context){
188+
var childGroup = parentGroup.newGroup(context);
189+
parentGroup.addGroup(childGroup);
190+
return childGroup;
191+
}
192+

test/watchgroup.spec.js

+106-6
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ describe('WatchGroup', function() {
313313

314314
it('should react when pure function return value changes in child watchGroup', function() {
315315
setup();
316-
var childWatchGrp = watchGrp.newGroup({'a': 1, 'b': 2});
316+
var childWatchGrp = createAndAddGroup(watchGrp, {'a': 1, 'b': 2});
317317

318318
var watch = childWatchGrp.watchExpression(new PureFunctionAST('add', function(a, b) {
319319
logger.log('+');
@@ -707,9 +707,9 @@ describe('WatchGroup', function() {
707707
proxy1 = Object.create(context);
708708
proxy2 = Object.create(context);
709709
proxy3 = Object.create(context);
710-
child1a = watchGrp.newGroup(proxy1);
711-
child1b = watchGrp.newGroup(proxy2);
712-
child2 = child1a.newGroup(proxy3);
710+
child1a = createAndAddGroup(watchGrp, proxy1);
711+
child1b = createAndAddGroup(watchGrp, proxy2);
712+
child2 = createAndAddGroup(child1a, proxy3);
713713

714714
child1a.watchExpression(parse('a'), logValue('1a'));
715715
child1b.watchExpression(parse('a'), logValue('1b'));
@@ -727,6 +727,98 @@ describe('WatchGroup', function() {
727727
proxy1 = proxy2 = proxy3 = child1a = child1b = child2 = null;
728728
});
729729

730+
it('should be able to add watches without being attached to a parent', () => {
731+
var changes = 0,
732+
context = {a:'something'};
733+
734+
setup();
735+
child1a = watchGrp.newGroup(context);
736+
child1a.watchExpression(parse('a'), logValue('1a'));
737+
738+
context.a = 'another';
739+
watchGrp.detectChanges(null, () => changes++);
740+
741+
expect(child1a.fieldCost).toBe(1);
742+
expect(watchGrp.totalFieldCost).toBe(0);
743+
expect(changes).toBe(0);
744+
expect(`${logger}`).toBe('1a');
745+
});
746+
747+
it('should be able to connect a group with existing watches', () => {
748+
var changes = 0,
749+
context = {a:'something'};
750+
751+
setup();
752+
child1a = watchGrp.newGroup(context);
753+
child1a.watchExpression(parse('a'), logValue('1a'));
754+
watchGrp.addGroup(child1a);
755+
756+
context.a = 'another';
757+
watchGrp.detectChanges(null, () => changes++);
758+
759+
expect(child1a.fieldCost).toBe(1);
760+
expect(watchGrp.totalFieldCost).toBe(1);
761+
expect(changes).toBe(1);
762+
expect(`${logger}`).toBe('1a');
763+
});
764+
765+
it('should be able to re-connect a group with existing watches', () => {
766+
var changes = 0,
767+
context = {a:'something'};
768+
769+
setup();
770+
child1a = watchGrp.newGroup(context);
771+
child1a.watchExpression(parse('a'), logValue('1a'));
772+
773+
watchGrp.addGroup(child1a);
774+
child1a.remove();
775+
776+
context.a = 'another';
777+
watchGrp.detectChanges(null, () => changes++);
778+
779+
expect(child1a.fieldCost).toBe(1);
780+
expect(watchGrp.totalFieldCost).toBe(0);
781+
expect(changes).toBe(0);
782+
783+
watchGrp.addGroup(child1a);
784+
785+
context.a = 'another2';
786+
watchGrp.detectChanges(null, () => changes++);
787+
788+
expect(child1a.fieldCost).toBe(1);
789+
expect(watchGrp.totalFieldCost).toBe(1);
790+
expect(changes).toBe(1);
791+
});
792+
793+
it('should be able to move a group with existing watches', () => {
794+
var changes = 0,
795+
context = {a:'something'};
796+
797+
setup();
798+
child1a = watchGrp.newGroup(context);
799+
child1a.watchExpression(parse('a'), logValue('1a'));
800+
801+
watchGrp.addGroup(child1a);
802+
child1a.remove();
803+
804+
context.a = 'another';
805+
watchGrp.detectChanges(null, () => changes++);
806+
807+
expect(child1a.fieldCost).toBe(1);
808+
expect(watchGrp.totalFieldCost).toBe(0);
809+
expect(changes).toBe(0);
810+
811+
child2 = watchGrp.newGroup({});
812+
watchGrp.addGroup(child2);
813+
child2.addGroup(child1a);
814+
815+
context.a = 'another2';
816+
watchGrp.detectChanges(null, () => changes++);
817+
818+
expect(child1a.fieldCost).toBe(1);
819+
expect(watchGrp.totalFieldCost).toBe(1);
820+
expect(changes).toBe(1);
821+
});
730822

731823
it('should set field cost to expected value', function() {
732824
setupChildGroups();
@@ -802,7 +894,7 @@ describe('WatchGroup', function() {
802894
it('should not call reaction function on removed group', function() {
803895
setup({ 'name': 'misko' });
804896

805-
var child = watchGrp.newGroup(context);
897+
var child = createAndAddGroup(watchGrp, context);
806898
watchGrp.watchExpression(parse('name'), function(v, p) {
807899
logger.log(`root ${v}`);
808900
if (v === 'destroy') {
@@ -831,7 +923,8 @@ describe('WatchGroup', function() {
831923
watchGrp.watchExpression(parse('a'), function(v, p) {
832924
logger.log(v);
833925
});
834-
watchGrp.newGroup(childContext).watchExpression(parse('b'), logCurrentValue);
926+
927+
createAndAddGroup(watchGrp, childContext).watchExpression(parse('b'), logCurrentValue);
835928

836929
watchGrp.detectChanges();
837930
expect(logger.toArray()).toEqual(['OK', 'OK']);
@@ -846,3 +939,10 @@ describe('WatchGroup', function() {
846939
});
847940
});
848941
});
942+
943+
function createAndAddGroup(parentGroup, context){
944+
var childGroup = parentGroup.newGroup(context);
945+
parentGroup.addGroup(childGroup);
946+
return childGroup;
947+
}
948+

0 commit comments

Comments
 (0)