Skip to content

Instantly share code, notes, and snippets.

Created June 9, 2012 21:44
Show Gist options
  • Save anonymous/2902693 to your computer and use it in GitHub Desktop.
Save anonymous/2902693 to your computer and use it in GitHub Desktop.
diff --git a/packages/ember-metal/lib/accessors.js b/packages/ember-metal/lib/accessors.js
index d4d7e6f..48930dd 100644
--- a/packages/ember-metal/lib/accessors.js
+++ b/packages/ember-metal/lib/accessors.js
@@ -34,18 +34,41 @@ var basicGet = function get(obj, keyName) {
}
};
+var watchedSet = function(obj, keyName, value) {
+ meta(obj).values[keyName] = value;
+};
+
+// if there are no getters, keep the raw property up to date
+if (!Ember.platform.hasPropertyAccessors) {
+ watchedSet = function(obj, keyName, value, values) {
+ obj[keyName] = value;
+ meta(obj).values[keyName] = value;
+ };
+}
+
+var META_KEY = Ember.META_KEY;
+
/** @private */
var basicSet = function set(obj, keyName, value) {
var isObject = 'object' === typeof obj;
var hasProp = isObject && !(keyName in obj);
+ var changed;
// setUnknownProperty is called if `obj` is an object,
// the property does not already exist, and the
// `setUnknownProperty` method exists on the object
- var unknownProp = hasProp && 'function' === typeof obj.setUnknownProperty;
+ var unknownProp = hasProp && 'function' === typeof obj.setUnknownProperty,
+ meta = obj[META_KEY];
if (unknownProp) {
obj.setUnknownProperty(keyName, value);
+ } else if (meta && meta.watching[keyName]) {
+ // only trigger a change if the value has changed
+ if (value !== obj[keyName]) {
+ Ember.propertyWillChange(obj, keyName);
+ watchedSet(obj, keyName, value);
+ Ember.propertyDidChange(obj, keyName);
+ }
} else {
obj[keyName] = value;
}
diff --git a/packages/ember-metal/lib/mixin.js b/packages/ember-metal/lib/mixin.js
index e194ac0..7040419 100644
--- a/packages/ember-metal/lib/mixin.js
+++ b/packages/ember-metal/lib/mixin.js
@@ -117,7 +117,6 @@ function mergeMixins(mixins, m, descs, values, base) {
value = baseValue ? baseValue.concat(value) : Ember.makeArray(value);
}
- descs[key] = Ember.SIMPLE_PROPERTY;
values[key] = value;
}
}
@@ -200,7 +199,7 @@ function applyMixin(obj, mixins, partial) {
//
// * Handle concatenated properties
// * Set up _super wrapping if necessary
- // * Set up descriptors (simple, watched or computed properties)
+ // * Set up computed property descriptors
// * Copying `toString` in broken browsers
mergeMixins(mixins, meta(obj), descs, values, obj);
@@ -209,8 +208,8 @@ function applyMixin(obj, mixins, partial) {
didApply = values.didApplyProperty || obj.didApplyProperty;
}
- for(key in descs) {
- if (!descs.hasOwnProperty(key)) { continue; }
+ for(key in values) {
+ if (!values.hasOwnProperty(key)) { continue; }
desc = descs[key];
value = values[key];
@@ -225,7 +224,7 @@ function applyMixin(obj, mixins, partial) {
req[key] = true;
}
} else {
- while (desc instanceof Alias) {
+ while (desc && desc instanceof Alias) {
var altKey = desc.methodName;
if (descs[altKey]) {
value = values[altKey];
@@ -235,12 +234,13 @@ function applyMixin(obj, mixins, partial) {
value = desc.val(obj, altKey);
} else {
value = obj[altKey];
- desc = Ember.SIMPLE_PROPERTY;
}
}
if (willApply) { willApply.call(obj, key); }
+ // If an observer replaces an existing superclass observer,
+ // remove the superclass observers.
var observerPaths = getObserverPaths(value),
curObserverPaths = observerPaths && getObserverPaths(obj[key]),
beforeObserverPaths = getBeforeObserverPaths(value),
diff --git a/packages/ember-metal/lib/properties.js b/packages/ember-metal/lib/properties.js
index ce00a17..c43a1c7 100644
--- a/packages/ember-metal/lib/properties.js
+++ b/packages/ember-metal/lib/properties.js
@@ -12,10 +12,10 @@ require('ember-metal/accessors');
var USE_ACCESSORS = Ember.USE_ACCESSORS,
GUID_KEY = Ember.GUID_KEY,
META_KEY = Ember.META_KEY,
- meta = Ember.meta,
+ EMPTY_META = Ember.EMPTY_META,
+ metaFor = Ember.meta,
o_create = Ember.create,
- objectDefineProperty = Ember.platform.defineProperty,
- SIMPLE_PROPERTY, WATCHED_PROPERTY;
+ objectDefineProperty = Ember.platform.defineProperty;
// ..........................................................
// DESCRIPTOR
@@ -23,6 +23,7 @@ var USE_ACCESSORS = Ember.USE_ACCESSORS,
/**
@private
+ EMPTY_META = Ember.EMPTY_META,
@constructor
Objects of this type can implement an interface to responds requests to
@@ -32,227 +33,11 @@ var USE_ACCESSORS = Ember.USE_ACCESSORS,
*/
var Descriptor = Ember.Descriptor = function() {};
-var setup = Descriptor.setup = function(obj, keyName, value) {
- objectDefineProperty(obj, keyName, {
- writable: true,
- configurable: true,
- enumerable: true,
- value: value
- });
-};
-
-var DescriptorPrototype = Ember.Descriptor.prototype;
-
-/**
- Called whenever we want to set the property value. Should set the value
- and return the actual set value (which is usually the same but may be
- different in the case of computed properties.)
-
- @param {Object} obj
- The object to set the value on.
-
- @param {String} keyName
- The key to set.
-
- @param {Object} value
- The new value
-
- @returns {Object} value actual set value
-*/
-DescriptorPrototype.set = function(obj, keyName, value) {
- obj[keyName] = value;
- return value;
-};
-
-/**
- Called whenever we want to get the property value. Should retrieve the
- current value.
-
- @param {Object} obj
- The object to get the value on.
-
- @param {String} keyName
- The key to retrieve
-
- @returns {Object} the current value
-*/
-DescriptorPrototype.get = function(obj, keyName) {
- return get(obj, keyName, obj);
-};
-
-/**
- This is called on the descriptor to set it up on the object. The
- descriptor is responsible for actually defining the property on the object
- here.
-
- The passed `value` is the transferValue returned from any previous
- descriptor.
-
- @param {Object} obj
- The object to set the value on.
-
- @param {String} keyName
- The key to set.
-
- @param {Object} value
- The transfer value from any previous descriptor.
-
- @returns {void}
-*/
-DescriptorPrototype.setup = setup;
-
-/**
- This is called on the descriptor just before another descriptor takes its
- place. This method should at least return the 'transfer value' of the
- property - which is the value you want to passed as the input to the new
- descriptor's setup() method.
-
- It is not generally necessary to actually 'undefine' the property as a new
- property descriptor will redefine it immediately after this method returns.
-
- @param {Object} obj
- The object to set the value on.
-
- @param {String} keyName
- The key to set.
-
- @returns {Object} transfer value
-*/
-DescriptorPrototype.teardown = function(obj, keyName) {
- return obj[keyName];
-};
-
-DescriptorPrototype.val = function(obj, keyName) {
- return obj[keyName];
-};
-
// ..........................................................
// SIMPLE AND WATCHED PROPERTIES
//
-// The exception to this is that any objects managed by Ember but not a descendant
-// of Ember.Object will not throw an exception, instead failing silently. This
-// prevent errors with other libraries that may attempt to access special
-// properties on standard objects like Array. Usually this happens when copying
-// an object by looping over all properties.
-//
-// QUESTION: What is this scenario exactly?
-var mandatorySetter = Ember.Descriptor.MUST_USE_SETTER = function() {
- if (this instanceof Ember.Object) {
- if (this.isDestroyed) {
- Ember.assert('You cannot set observed properties on destroyed objects', false);
- } else {
- Ember.assert('Must use Ember.set() to access this property', false);
- }
- }
-};
-
-var WATCHED_DESC = {
- configurable: true,
- enumerable: true,
- set: mandatorySetter
-};
-
/** @private */
-function rawGet(obj, keyName, values) {
- var ret = values[keyName];
- if (ret === undefined && obj.unknownProperty) {
- ret = obj.unknownProperty(keyName);
- }
- return ret;
-}
-
-function get(obj, keyName) {
- return rawGet(obj, keyName, obj);
-}
-
-var emptyObject = {};
-
-function watchedGet(obj, keyName) {
- return rawGet(obj, keyName, meta(obj, false).values || emptyObject);
-}
-
-var hasGetters = Ember.platform.hasPropertyAccessors, rawSet;
-
-rawSet = function(obj, keyName, value, values) {
- values[keyName] = value;
-};
-
-// if there are no getters, keep the raw property up to date
-if (!Ember.platform.hasPropertyAccessors) {
- rawSet = function(obj, keyName, value, values) {
- obj[keyName] = value;
- values[keyName] = value;
- };
-}
-
-/** @private */
-function watchedSet(obj, keyName, value) {
- var m = meta(obj),
- values = m.values,
- changed = value !== values[keyName];
-
- if (changed) {
- Ember.propertyWillChange(obj, keyName);
- rawSet(obj, keyName, value, m.values);
- Ember.propertyDidChange(obj, keyName);
- }
-
- return value;
-}
-
-/** @private */
-function makeWatchedGetter(keyName) {
- return function() {
- return watchedGet(this, keyName);
- };
-}
-
-/** @private */
-function makeWatchedSetter(keyName) {
- return function(value) {
- return watchedSet(this, keyName, value);
- };
-}
-
-/**
- @private
-
- Private version of simple property that invokes property change callbacks.
-*/
-WATCHED_PROPERTY = new Ember.Descriptor();
-WATCHED_PROPERTY.get = watchedGet ;
-WATCHED_PROPERTY.set = watchedSet ;
-
-WATCHED_PROPERTY.setup = function(obj, keyName, value) {
- objectDefineProperty(obj, keyName, {
- configurable: true,
- enumerable: true,
- set: mandatorySetter,
- get: makeWatchedGetter(keyName)
- });
-
- meta(obj).values[keyName] = value;
-};
-
-WATCHED_PROPERTY.teardown = function(obj, keyName) {
- var ret = meta(obj).values[keyName];
- delete meta(obj).values[keyName];
- return ret;
-};
-
-/**
- The default descriptor for simple properties. Pass as the third argument
- to Ember.defineProperty() along with a value to set a simple value.
-
- @static
- @default Ember.Descriptor
-*/
-Ember.SIMPLE_PROPERTY = new Ember.Descriptor();
-SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY;
-
-SIMPLE_PROPERTY.unwatched = WATCHED_PROPERTY.unwatched = SIMPLE_PROPERTY;
-SIMPLE_PROPERTY.watched = WATCHED_PROPERTY.watched = WATCHED_PROPERTY;
// ..........................................................
// DEFINING PROPERTIES API
@@ -264,6 +49,18 @@ function hasDesc(descs, keyName) {
else return !!descs[keyName];
}
+var extractValue = function(obj, keyName, watching) {
+ if (watching) {
+ var values = metaFor(obj).values,
+ ret = values[keyName];
+
+ delete values[keyName];
+ return ret;
+ } else {
+ return obj[keyName];
+ }
+};
+
/**
@private
@@ -299,45 +96,60 @@ function hasDesc(descs, keyName) {
}).property('firstName', 'lastName').cacheable());
*/
Ember.defineProperty = function(obj, keyName, desc, val) {
- var m = meta(obj, false),
- descs = m.descs,
- watching = m.watching[keyName]>0,
+ var meta = obj[META_KEY] || EMPTY_META,
+ descs = meta && meta.descs,
+ watching = meta.watching[keyName],
+ descriptor = desc instanceof Ember.Descriptor,
override = true;
- if (val === undefined) {
+ var existingDesc = hasDesc(descs, keyName);
+
+ if (val === undefined && descriptor) {
// if a value wasn't provided, the value is the old value
// (which can be obtained by calling teardown on a property
// with a descriptor).
override = false;
- val = hasDesc(descs, keyName) ? descs[keyName].teardown(obj, keyName) : obj[keyName];
- } else if (hasDesc(descs, keyName)) {
+
+ if (existingDesc) { val = descs[keyName].teardown(obj, keyName); }
+ else { val = extractValue(obj, keyName, watching); }
+
+ } else if (existingDesc) {
// otherwise, tear down the descriptor, but use the provided
// value as the new value instead of the descriptor's current
// value.
descs[keyName].teardown(obj, keyName);
}
- if (!desc) {
- desc = SIMPLE_PROPERTY;
- }
-
- if (desc instanceof Ember.Descriptor) {
- m = meta(obj, true);
- descs = m.descs;
+ if (descriptor) {
+ meta = metaFor(obj);
+ descs = meta.descs;
- desc = (watching ? desc.watched : desc.unwatched) || desc;
descs[keyName] = desc;
- desc.setup(obj, keyName, val, watching);
+ desc.setup(obj, keyName, val);
- // compatibility with ES5
} else {
- if (descs[keyName]) meta(obj).descs[keyName] = null;
- objectDefineProperty(obj, keyName, desc);
+ if (descs[keyName]) { metaFor(obj).descs[keyName] = null; }
+
+ if (desc === undefined) {
+ if (existingDesc) {
+ objectDefineProperty(obj, keyName, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: val
+ });
+ } else {
+ obj[keyName] = val;
+ }
+ } else {
+ // compatibility with ES5
+ objectDefineProperty(obj, keyName, desc);
+ }
}
// if key is being watched, override chains that
// were initialized with the prototype
- if (override && watching) Ember.overrideChains(obj, keyName, m);
+ if (override && watching) { Ember.overrideChains(obj, keyName, meta); }
return this;
};
diff --git a/packages/ember-metal/lib/utils.js b/packages/ember-metal/lib/utils.js
index bec0bf4..49be05f 100644
--- a/packages/ember-metal/lib/utils.js
+++ b/packages/ember-metal/lib/utils.js
@@ -137,6 +137,8 @@ var EMPTY_META = {
watching: {}
};
+Ember.EMPTY_META = EMPTY_META;
+
if (Object.freeze) Object.freeze(EMPTY_META);
var createMeta = Ember.platform.defineProperty.isSimulated ? o_create : (function(meta) { return meta; });
diff --git a/packages/ember-metal/lib/watching.js b/packages/ember-metal/lib/watching.js
index 77a23a7..8f2da28 100644
--- a/packages/ember-metal/lib/watching.js
+++ b/packages/ember-metal/lib/watching.js
@@ -13,7 +13,7 @@ require('ember-metal/observer');
require('ember-metal/array');
var guidFor = Ember.guidFor,
- meta = Ember.meta,
+ metaFor = Ember.meta,
get = Ember.get,
set = Ember.set,
normalizeTuple = Ember.normalizeTuple.primitive,
@@ -92,7 +92,7 @@ function dependentKeysDidChange(obj, depKey, meta) {
/** @private */
function addChainWatcher(obj, keyName, node) {
if (!obj || ('object' !== typeof obj)) return; // nothing to do
- var m = meta(obj);
+ var m = metaFor(obj);
var nodes = m.chainWatchers;
if (!nodes || nodes.__emberproto__ !== obj) {
nodes = m.chainWatchers = { __emberproto__: obj };
@@ -106,7 +106,7 @@ function addChainWatcher(obj, keyName, node) {
/** @private */
function removeChainWatcher(obj, keyName, node) {
if (!obj || 'object' !== typeof obj) { return; } // nothing to do
- var m = meta(obj, false),
+ var m = metaFor(obj, false),
nodes = m.chainWatchers;
if (!nodes || nodes.__emberproto__ !== obj) { return; } //nothing to do
if (nodes[keyName]) { delete nodes[keyName][guidFor(node)]; }
@@ -132,7 +132,7 @@ function flushPendingChains() {
/** @private */
function isProto(pvalue) {
- return meta(pvalue, false).proto === pvalue;
+ return metaFor(pvalue, false).proto === pvalue;
}
// A ChainNode watches a single key on an object. If you provide a starting
@@ -368,7 +368,7 @@ ChainNodePrototype.didChange = function(suppressEvent) {
// the current object.
/** @private */
function chainsFor(obj) {
- var m = meta(obj), ret = m.chains;
+ var m = metaFor(obj), ret = m.chains;
if (!ret) {
ret = m.chains = new ChainNode(null, null, obj);
} else if (ret.value() !== obj) {
@@ -410,7 +410,56 @@ function chainsDidChange(obj, keyName, m) {
// WATCH
//
-var WATCHED_PROPERTY = Ember.SIMPLE_PROPERTY.watched;
+
+// The exception to this is that any objects managed by Ember but not a descendant
+// of Ember.Object will not throw an exception, instead failing silently. This
+// prevent errors with other libraries that may attempt to access special
+// properties on standard objects like Array. Usually this happens when copying
+// an object by looping over all properties.
+//
+// QUESTION: What is this scenario exactly?
+var mandatorySetter = Ember.Descriptor.MUST_USE_SETTER = function() {
+ if (Ember.Object && this instanceof Ember.Object) {
+ if (this.isDestroyed) {
+ Ember.assert('You cannot set observed properties on destroyed objects', false);
+ } else {
+ Ember.assert('Must use Ember.set() to access this property', false);
+ }
+ }
+};
+
+var switchToWatched = function(obj, keyName, meta) {
+ var value = obj[keyName];
+ meta.values[keyName] = value;
+
+ if (Ember.platform.hasPropertyAccessors) {
+ var desc = {
+ configurable: true,
+ enumerable: true,
+ set: mandatorySetter,
+ get: function(key) {
+ return meta.values[keyName];
+ }
+ };
+
+ Ember.platform.defineProperty(obj, keyName, desc);
+ }
+};
+
+var switchToUnwatched = function(obj, keyName, meta) {
+ var value = obj[keyName];
+ delete meta.values[keyName];
+
+ if (Ember.platform.hasPropertyAccessors) {
+ var desc = {
+ configurable: true,
+ enumerable: true,
+ value: value
+ };
+
+ Ember.platform.defineProperty(obj, keyName, desc);
+ }
+};
/**
@private
@@ -425,7 +474,7 @@ Ember.watch = function(obj, keyName) {
// can't watch length on Array - it is special...
if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; }
- var m = meta(obj), watching = m.watching, desc;
+ var m = metaFor(obj), watching = m.watching, desc;
// activate watching first time
if (!watching[keyName]) {
@@ -435,9 +484,7 @@ Ember.watch = function(obj, keyName) {
obj.willWatchProperty(keyName);
}
- desc = m.descs[keyName];
- desc = desc ? desc.watched : WATCHED_PROPERTY;
- if (desc) { Ember.defineProperty(obj, keyName, desc); }
+ if (!desc) { switchToWatched(obj, keyName, m); }
} else {
chainsFor(obj).add(keyName);
}
@@ -449,7 +496,7 @@ Ember.watch = function(obj, keyName) {
};
Ember.isWatching = function(obj, keyName) {
- return !!meta(obj).watching[keyName];
+ return !!metaFor(obj).watching[keyName];
};
Ember.watch.flushPending = flushPendingChains;
@@ -459,14 +506,15 @@ Ember.unwatch = function(obj, keyName) {
// can't watch length on Array - it is special...
if (keyName === 'length' && Ember.typeOf(obj) === 'array') { return this; }
- var watching = meta(obj).watching, desc, descs;
+ var watching = metaFor(obj).watching, desc, descs;
if (watching[keyName] === 1) {
watching[keyName] = 0;
if (isKeyName(keyName)) {
- desc = meta(obj).descs[keyName];
- desc = desc ? desc.unwatched : SIMPLE_PROPERTY;
- if (desc) { Ember.defineProperty(obj, keyName, desc); }
+ var meta = metaFor(obj);
+ desc = meta.descs[keyName];
+
+ if (!desc) { switchToUnwatched(obj, keyName, meta); }
if ('function' === typeof obj.didUnwatchProperty) {
obj.didUnwatchProperty(keyName);
@@ -490,7 +538,7 @@ Ember.unwatch = function(obj, keyName) {
safe to call multiple times.
*/
Ember.rewatch = function(obj) {
- var m = meta(obj, false), chains = m.chains, bindings = m.bindings, key, b;
+ var m = metaFor(obj, false), chains = m.chains, bindings = m.bindings, key, b;
// make sure the object has its own guid.
if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) {
@@ -527,7 +575,7 @@ Ember.rewatch = function(obj) {
@returns {void}
*/
function propertyWillChange(obj, keyName, value) {
- var m = meta(obj, false),
+ var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
@@ -562,7 +610,7 @@ Ember.propertyWillChange = propertyWillChange;
@returns {void}
*/
function propertyDidChange(obj, keyName) {
- var m = meta(obj, false),
+ var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
diff --git a/packages/ember-metal/tests/binding/connect_test.js b/packages/ember-metal/tests/binding/connect_test.js
index 081ddb0..88d1aab 100644
--- a/packages/ember-metal/tests/binding/connect_test.js
+++ b/packages/ember-metal/tests/binding/connect_test.js
@@ -38,6 +38,8 @@ function performTest(binding, a, b, get, set, connect) {
equal(get(b, 'bar'), 'BARF', 'a should have changed');
}
+module("Ember.Binding");
+
testBoth('Connecting a binding between two properties', function(get, set) {
var a = { foo: 'FOO', bar: 'BAR' };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment