Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PERF beta] @each should remain a stable node for chains. #11990

Merged
merged 1 commit into from
Aug 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions packages/ember-metal/lib/chains.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,6 @@ function ChainNode(parent, key, value) {
addChainWatcher(this._object, this._key, this);
}
}

// Special-case: the EachProxy relies on immediate evaluation to
// establish its observers.
//
// TODO: Replace this with an efficient callback that the EachProxy
// can implement.
if (this._parent && this._parent._key === '@each') {
this.value();
}
}

function lazyGet(obj, key) {
Expand All @@ -218,7 +209,7 @@ function lazyGet(obj, key) {
}

// Use `get` if the return value is an EachProxy or an uncacheable value.
if (key === '@each' || isVolatile(obj[key])) {
if (isVolatile(obj[key])) {
return get(obj, key);
// Otherwise attempt to get the cached value of the computed property
} else {
Expand Down Expand Up @@ -375,12 +366,6 @@ ChainNode.prototype = {
addChainWatcher(obj, this._key, this);
}
this._value = undefined;

// Special-case: the EachProxy relies on immediate evaluation to
// establish its observers.
if (this._parent && this._parent._key === '@each') {
this.value();
}
}

// then notify chains...
Expand Down
2 changes: 1 addition & 1 deletion packages/ember-metal/lib/expand_properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var SPLIT_REGEX = /\{|\}/;
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar'
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz'
Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz'
Ember.expandProperties('foo.{bar,baz}.@each', echo) //=> 'foo.bar.@each', 'foo.baz.@each'
Ember.expandProperties('foo.{bar,baz}.[]', echo) //=> 'foo.bar.[]', 'foo.baz.[]'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funky whitespace

Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs'
Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz'
```
Expand Down
5 changes: 3 additions & 2 deletions packages/ember-metal/tests/expand_properties_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ QUnit.test('Properties without expansions are unaffected', function() {

expandProperties('a', addProperty);
expandProperties('a.b', addProperty);
expandProperties('a.b.@each', addProperty);
expandProperties('a.b.[]', addProperty);
expandProperties('a.b.@each.c', addProperty);

deepEqual(['a', 'a.b', 'a.b.@each'].sort(), foundProperties.sort());
deepEqual(['a', 'a.b', 'a.b.[]', 'a.b.@each.c'].sort(), foundProperties.sort());
});

QUnit.test('A single expansion at the end expands properly', function() {
Expand Down
23 changes: 11 additions & 12 deletions packages/ember-runtime/lib/mixins/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
sendEvent,
hasListeners
} from 'ember-metal/events';
import { isWatching } from 'ember-metal/watching';
import EachProxy from 'ember-runtime/system/each_proxy';

function arrayObserversHelper(obj, target, opts, operation, notify) {
var willChange = (opts && opts.willChange) || 'arrayWillChange';
Expand Down Expand Up @@ -422,13 +422,13 @@ export default Mixin.create(Enumerable, {
}
}

// Make sure the @each proxy is set up if anyone is observing @each
if (isWatching(this, '@each')) {
get(this, '@each');
if (this.__each) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is __each set? Is it non-enumerable?

this.__each.arrayWillChange(this, startIdx, removeAmt, addAmt);
}

sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]);


if (startIdx >= 0 && removeAmt >= 0 && get(this, 'hasEnumerableObservers')) {
removing = [];
lim = startIdx + removeAmt;
Expand Down Expand Up @@ -489,6 +489,11 @@ export default Mixin.create(Enumerable, {
}

this.enumerableContentDidChange(removeAmt, adding);

if (this.__each) {
this.__each.arrayDidChange(this, startIdx, removeAmt, addAmt);
}

sendEvent(this, '@array:change', [this, startIdx, removeAmt, addAmt]);

var length = get(this, 'length');
Expand All @@ -508,10 +513,6 @@ export default Mixin.create(Enumerable, {
return this;
},

// ..........................................................
// ENUMERATED PROPERTIES
//

/**
Returns a special object that can be used to observe individual properties
on the array. Just get an equivalent property on this object and it will
Expand All @@ -525,13 +526,11 @@ export default Mixin.create(Enumerable, {
@public
*/
'@each': computed(function() {
// TODO use Symbol or add to meta
if (!this.__each) {
// ES6TODO: GRRRRR
var EachProxy = requireModule('ember-runtime/system/each_proxy')['EachProxy'];

this.__each = new EachProxy(this);
}

return this.__each;
})
}).volatile()
});
212 changes: 54 additions & 158 deletions packages/ember-runtime/lib/system/each_proxy.js
Original file line number Diff line number Diff line change
@@ -1,194 +1,72 @@
/**
@module ember
@submodule ember-runtime
*/

import Ember from 'ember-metal/core'; // Ember.assert

import { get } from 'ember-metal/property_get';
import { guidFor } from 'ember-metal/utils';
import { typeOf } from 'ember-runtime/utils';
import EmberArray from 'ember-runtime/mixins/array'; // ES6TODO: WAT? Circular dep?
import EmberObject from 'ember-runtime/system/object';
import { computed } from 'ember-metal/computed';
import {
addObserver,
_addBeforeObserver,
_removeBeforeObserver,
addObserver,
removeObserver
} from 'ember-metal/observer';
import { watchedEvents } from 'ember-metal/events';
import { defineProperty } from 'ember-metal/properties';
import {
beginPropertyChanges,
propertyDidChange,
propertyWillChange,
endPropertyChanges,
changeProperties
propertyWillChange
} from 'ember-metal/property_events';

var EachArray = EmberObject.extend(EmberArray, {

init(content, keyName, owner) {
this._super(...arguments);
this._keyName = keyName;
this._owner = owner;
this._content = content;
},

objectAt(idx) {
var item = this._content.objectAt(idx);
return item && get(item, this._keyName);
},

length: computed(function() {
var content = this._content;
return content ? get(content, 'length') : 0;
})

});

var IS_OBSERVER = /^.+:(before|change)$/;

function addObserverForContentKey(content, keyName, proxy, idx, loc) {
var objects = proxy._objects;
var guid;
if (!objects) {
objects = proxy._objects = {};
}

while (--loc >= idx) {
var item = content.objectAt(loc);
if (item) {
Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', typeOf(item) === 'instance' || typeOf(item) === 'object');
_addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
addObserver(item, keyName, proxy, 'contentKeyDidChange');

// keep track of the index each item was found at so we can map
// it back when the obj changes.
guid = guidFor(item);
if (!objects[guid]) {
objects[guid] = [];
}

objects[guid].push(loc);
}
}
}

function removeObserverForContentKey(content, keyName, proxy, idx, loc) {
var objects = proxy._objects;
if (!objects) {
objects = proxy._objects = {};
}

var indices, guid;

while (--loc >= idx) {
var item = content.objectAt(loc);
if (item) {
_removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
removeObserver(item, keyName, proxy, 'contentKeyDidChange');

guid = guidFor(item);
indices = objects[guid];
indices[indices.indexOf(loc)] = null;
}
}
}

/**
This is the object instance returned when you get the `@each` property on an
array. It uses the unknownProperty handler to automatically create
EachArray instances for property names.
@class EachProxy
@private
*/
var EachProxy = EmberObject.extend({

init(content) {
this._super(...arguments);
this._content = content;
content.addArrayObserver(this);

// in case someone is already observing some keys make sure they are
// added
watchedEvents(this).forEach((eventName) => {
this.didAddListener(eventName);
});
},

/**
You can directly access mapped properties by simply requesting them.
The `unknownProperty` handler will generate an EachArray of each item.

@method unknownProperty
@param keyName {String}
@param value {*}
@private
*/
unknownProperty(keyName, value) {
var ret = new EachArray(this._content, keyName, this);
defineProperty(this, keyName, null, ret);
this.beginObservingContentKey(keyName);
return ret;
},
function EachProxy(content) {
this._content = content;
this._keys = undefined;
this.__ember_meta__ = undefined;
}

EachProxy.prototype = {
// ..........................................................
// ARRAY CHANGES
// Invokes whenever the content array itself changes.

arrayWillChange(content, idx, removedCnt, addedCnt) {
var keys = this._keys;
var key, lim;

lim = removedCnt>0 ? idx+removedCnt : -1;
beginPropertyChanges(this);

for (key in keys) {
if (!keys.hasOwnProperty(key)) { continue; }

if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); }

let keys = this._keys;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this._keys ever legitimately contain prototype extensions? If not, maybe we should make it an EmptyObject. This would allow us to also skip the bellow !keys.hasOwnProperty(key)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a here it is: https://github.com/emberjs/ember.js/blob/fix-each-beta/packages/ember-runtime/lib/system/each_proxy.js#L79

ya this should be one of those EmptyObjects and we can take the fast-path for x in y without HOP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a empty, this is beta though

let lim = removedCnt > 0 ? idx + removedCnt : -1;
for (let key in keys) {
if (!keys.hasOwnProperty(key)) {
continue;
}
if (lim>0) {
removeObserverForContentKey(content, key, this, idx, lim);
}
propertyWillChange(this, key);
}

propertyWillChange(this._content, '@each');
endPropertyChanges(this);
},

arrayDidChange(content, idx, removedCnt, addedCnt) {
var keys = this._keys;
var lim;

lim = addedCnt>0 ? idx+addedCnt : -1;
changeProperties(function() {
for (var key in keys) {
if (!keys.hasOwnProperty(key)) { continue; }

if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); }

propertyDidChange(this, key);
let keys = this._keys;
let lim = addedCnt>0 ? idx+addedCnt : -1;
for (let key in keys) {
if (!keys.hasOwnProperty(key)) {
continue;
}

propertyDidChange(this._content, '@each');
}, this);
if (lim>0) {
addObserverForContentKey(content, key, this, idx, lim);
}
propertyDidChange(this, key);
}
},

// ..........................................................
// LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS
// Start monitoring keys based on who is listening...

didAddListener(eventName) {
if (IS_OBSERVER.test(eventName)) {
this.beginObservingContentKey(eventName.slice(0, -7));
}
willWatchProperty(property) {
this.beginObservingContentKey(property);
},

didRemoveListener(eventName) {
if (IS_OBSERVER.test(eventName)) {
this.stopObservingContentKey(eventName.slice(0, -7));
}
didUnwatchProperty(property) {
this.stopObservingContentKey(property);
},

// ..........................................................
Expand Down Expand Up @@ -229,9 +107,27 @@ var EachProxy = EmberObject.extend({
contentKeyDidChange(obj, keyName) {
propertyDidChange(this, keyName);
}
});

export {
EachArray,
EachProxy
};

function addObserverForContentKey(content, keyName, proxy, idx, loc) {
while (--loc >= idx) {
var item = content.objectAt(loc);
if (item) {
Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', typeof item === 'object');
_addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
addObserver(item, keyName, proxy, 'contentKeyDidChange');
}
}
}

function removeObserverForContentKey(content, keyName, proxy, idx, loc) {
while (--loc >= idx) {
var item = content.objectAt(loc);
if (item) {
_removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange');
removeObserver(item, keyName, proxy, 'contentKeyDidChange');
}
}
}

export default EachProxy;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be

export default function EachProxy (...) { .. };

Loading