Skip to content

Commit 95430d1

Browse files
Chris Garrettkategengler
authored andcommitted
[BUGFIX release] Make ArrayProxy Lazy
This PR refactors ArrayProxy to only begin observing the proxied array's changes the first time the proxy is actually used. This fixes a number of bugs that have popped up recently, and also ensures that ArrayProxy creation does not accidentally entangle with the autotracking stack. It also simplifies the code overall, and conceptually is more in line with the way that autotracking works. In addition, it adds a CUSTOM_TAG_FOR to the proxy, to forward the `[]` and `length` tags from its content directly rather than attempting to managed them itself. This keeps the system from encountering issues related to tag updates, and from needing to re-notify when changes are propagated. As part of fixing this, I also discovered that the `hasArrayObservers` API has been broken since ~3.11. I fixed it and added a test. (cherry picked from commit 8bbedfa)
1 parent 6ff73f9 commit 95430d1

File tree

7 files changed

+97
-30
lines changed

7 files changed

+97
-30
lines changed

packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,31 @@ moduleFor(
6969
this.assertText('123');
7070
}
7171

72+
'@test creating an array proxy inside a tracking context and immediately updating its content before usage does not trigger backtracking assertion'() {
73+
class LoaderComponent extends GlimmerishComponent {
74+
get data() {
75+
if (!this._data) {
76+
this._data = ArrayProxy.create({
77+
content: A(),
78+
});
79+
80+
this._data.content.pushObjects([1, 2, 3]);
81+
}
82+
83+
return this._data;
84+
}
85+
}
86+
87+
this.registerComponent('loader', {
88+
ComponentClass: LoaderComponent,
89+
template: '{{#each this.data as |item|}}{{item}}{{/each}}',
90+
});
91+
92+
this.render('<Loader/>');
93+
94+
this.assertText('123');
95+
}
96+
7297
'@test tracked properties that are uninitialized do not throw an error'() {
7398
let CountComponent = Component.extend({
7499
count: tracked(),

packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers';
22

3-
import { get, set, notifyPropertyChange, computed } from '@ember/-internals/metal';
3+
import { get, set, notifyPropertyChange, computed, on } from '@ember/-internals/metal';
44
import { A as emberA, ArrayProxy, RSVP } from '@ember/-internals/runtime';
55
import { HAS_NATIVE_SYMBOL } from '@ember/-internals/utils';
66

@@ -1108,6 +1108,22 @@ moduleFor(
11081108
}
11091109
);
11101110

1111+
moduleFor(
1112+
'Syntax test: {{#each}} with array proxies, content is updated after init',
1113+
class extends EachTest {
1114+
createList(items) {
1115+
let wrapped = emberA(items);
1116+
let proxy = ArrayProxy.extend({
1117+
setup: on('init', function() {
1118+
this.set('content', emberA(wrapped));
1119+
}),
1120+
}).create();
1121+
1122+
return { list: proxy, delegate: wrapped };
1123+
}
1124+
}
1125+
);
1126+
11111127
moduleFor(
11121128
'Syntax test: {{#each as}} undefined path',
11131129
class extends RenderingTestCase {

packages/@ember/-internals/runtime/lib/mixins/array.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ const ArrayMixin = Mixin.create(Enumerable, {
569569
configurable: true,
570570
enumerable: false,
571571
get() {
572-
hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
572+
return hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
573573
},
574574
}),
575575

packages/@ember/-internals/runtime/lib/system/array_proxy.js

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ import {
1111
removeArrayObserver,
1212
replace,
1313
getChainTagsForKey,
14-
tagForProperty,
14+
CUSTOM_TAG_FOR,
1515
arrayContentDidChange,
16-
arrayContentWillChange,
1716
} from '@ember/-internals/metal';
1817
import EmberObject from './object';
1918
import { isArray, MutableArray } from '../mixins/array';
2019
import { assert } from '@ember/debug';
21-
import { combine, update, validate, value } from '@glimmer/validator';
20+
import { combine, validate, value, tagFor } from '@glimmer/validator';
2221

2322
const ARRAY_OBSERVER_MAPPING = {
2423
willChange: '_arrangedContentArrayWillChange',
@@ -106,18 +105,24 @@ export default class ArrayProxy extends EmberObject {
106105
this._length = 0;
107106

108107
this._arrangedContent = null;
109-
110108
this._arrangedContentIsUpdating = false;
111-
this._arrangedContentTag = combine(getChainTagsForKey(this, 'arrangedContent'));
112-
this._arrangedContentRevision = value(this._arrangedContentTag);
109+
this._arrangedContentTag = null;
110+
this._arrangedContentRevision = null;
111+
}
113112

114-
this._addArrangedContentArrayObserver();
113+
[PROPERTY_DID_CHANGE]() {
114+
this._revalidate();
115+
}
115116

116-
update(tagForProperty(this, '[]'), combine(getChainTagsForKey(this, 'arrangedContent.[]')));
117-
update(
118-
tagForProperty(this, 'length'),
119-
combine(getChainTagsForKey(this, 'arrangedContent.length'))
120-
);
117+
[CUSTOM_TAG_FOR](key) {
118+
if (key === '[]' || key === 'length') {
119+
// revalidate eagerly if we're being tracked, since we no longer will
120+
// be able to later on due to backtracking re-render assertion
121+
this._revalidate();
122+
return combine(getChainTagsForKey(this, `arrangedContent.${key}`));
123+
}
124+
125+
return tagFor(this, key);
121126
}
122127

123128
willDestroy() {
@@ -236,10 +241,6 @@ export default class ArrayProxy extends EmberObject {
236241
}
237242
}
238243

239-
[PROPERTY_DID_CHANGE]() {
240-
this._revalidate();
241-
}
242-
243244
_updateArrangedContentArray() {
244245
let oldLength = this._objects === null ? 0 : this._objects.length;
245246
let arrangedContent = get(this, 'arrangedContent');
@@ -301,13 +302,21 @@ export default class ArrayProxy extends EmberObject {
301302
}
302303

303304
_revalidate() {
305+
if (this._arrangedContentIsUpdating === true) return;
306+
304307
if (
305-
!this._arrangedContentIsUpdating &&
308+
this._arrangedContentTag === null ||
306309
!validate(this._arrangedContentTag, this._arrangedContentRevision)
307310
) {
308-
this._arrangedContentIsUpdating = true;
309-
this._updateArrangedContentArray();
310-
this._arrangedContentIsUpdating = false;
311+
if (this._arrangedContentTag === null) {
312+
// This is the first time the proxy has been setup, only add the observer
313+
// don't trigger any events
314+
this._addArrangedContentArrayObserver();
315+
} else {
316+
this._arrangedContentIsUpdating = true;
317+
this._updateArrangedContentArray();
318+
this._arrangedContentIsUpdating = false;
319+
}
311320

312321
this._arrangedContentTag = combine(getChainTagsForKey(this, 'arrangedContent'));
313322
this._arrangedContentRevision = value(this._arrangedContentTag);
@@ -328,10 +337,6 @@ ArrayProxy.reopen(MutableArray, {
328337

329338
// Array proxies don't need to notify when they change since their `[]` tag is
330339
// already dependent on the `[]` tag of `arrangedContent`
331-
arrayContentWillChange(startIdx, removeAmt, addAmt) {
332-
return arrayContentWillChange(this, startIdx, removeAmt, addAmt, false);
333-
},
334-
335340
arrayContentDidChange(startIdx, removeAmt, addAmt) {
336341
return arrayContentDidChange(this, startIdx, removeAmt, addAmt, false);
337342
},

packages/@ember/-internals/runtime/tests/helpers/array.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export function runArrayTests(name, Tests, ...types) {
314314
moduleFor(`EmberArray: ${name}`, Tests, EmberArrayHelpers);
315315
break;
316316
case 'MutableArray':
317-
moduleFor(`MutableArray: ${name}`, Tests, EmberArrayHelpers);
317+
moduleFor(`MutableArray: ${name}`, Tests, MutableArrayHelpers);
318318
break;
319319
case 'CopyableArray':
320320
moduleFor(`CopyableArray: ${name}`, Tests, CopyableArray);
@@ -323,7 +323,7 @@ export function runArrayTests(name, Tests, ...types) {
323323
moduleFor(`CopyableNativeArray: ${name}`, Tests, CopyableNativeArray);
324324
break;
325325
case 'NativeArray':
326-
moduleFor(`NativeArray: ${name}`, Tests, EmberArrayHelpers);
326+
moduleFor(`NativeArray: ${name}`, Tests, NativeArrayHelpers);
327327
break;
328328
}
329329
});

packages/@ember/-internals/runtime/tests/mixins/array_test.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
arrayContentWillChange,
1212
} from '@ember/-internals/metal';
1313
import EmberObject from '../../lib/system/object';
14-
import EmberArray from '../../lib/mixins/array';
15-
import { A as emberA } from '../../lib/mixins/array';
14+
import EmberArray, { A as emberA } from '../../lib/mixins/array';
1615
import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers';
1716

1817
/*
@@ -254,6 +253,22 @@ moduleFor(
254253
arrayContentDidChange(obj);
255254
assert.deepEqual(observer._after, null);
256255
}
256+
257+
['@test hasArrayObservers should work'](assert) {
258+
assert.equal(
259+
obj.hasArrayObservers,
260+
true,
261+
'correctly shows it has an array observer when one exists'
262+
);
263+
264+
removeArrayObserver(obj, observer);
265+
266+
assert.equal(
267+
obj.hasArrayObservers,
268+
false,
269+
'correctly shows it has an array observer when one exists'
270+
);
271+
}
257272
}
258273
);
259274

packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ moduleFor(
3838
let content2 = A();
3939
let proxy = ArrayProxy.create({ content: content1 });
4040

41+
assert.deepEqual(sortedListenersFor(content1, '@array:before'), []);
42+
assert.deepEqual(sortedListenersFor(content1, '@array:change'), []);
43+
44+
// setup proxy
45+
proxy.length;
46+
4147
assert.deepEqual(sortedListenersFor(content1, '@array:before'), [
4248
'_arrangedContentArrayWillChange',
4349
]);

0 commit comments

Comments
 (0)