From e6c4ba0eec0e2032bebdfb6b4513a7329b504963 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Fri, 25 Mar 2016 19:51:56 -0700 Subject: [PATCH] Give Glimmer observer-less efficient updates Previously, the Glimmer 2 integration with Ember would recompute values in curlies on each top-down revalidation. This commit uses new features of the Glimmer engine to allow it to more cheaply revalidate a reference, and often skip the computation if the reference is sure the value cannot have changed. --- ember-cli-build.js | 1 + lib/packages.js | 1 + package.json | 2 +- .../lib/components/curly-component.js | 59 +++-- .../lib/components/dynamic-component.js | 5 +- .../ember-glimmer/lib/components/outlet.js | 28 +-- .../lib/ember-routing-view/index.js | 22 +- packages/ember-glimmer/lib/environment.js | 15 +- packages/ember-glimmer/lib/helper.js | 13 +- .../ember-glimmer/lib/helpers/if-unless.js | 12 +- .../ember-glimmer/lib/utils/references.js | 227 +++++++++++------- packages/ember-metal/lib/meta.js | 7 +- packages/ember-metal/lib/property_set.js | 8 +- packages/ember-metal/lib/tags.js | 32 +++ packages/ember-runtime/lib/mixins/-proxy.js | 9 + packages/ember-runtime/lib/mixins/array.js | 3 + 16 files changed, 277 insertions(+), 167 deletions(-) create mode 100644 packages/ember-metal/lib/tags.js diff --git a/ember-cli-build.js b/ember-cli-build.js index ec4a073f2e8..bc634164f89 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -77,6 +77,7 @@ module.exports = function() { addGlimmerPackage(vendorPackages, 'glimmer'); addGlimmerPackage(vendorPackages, 'glimmer-compiler'); addGlimmerPackage(vendorPackages, 'glimmer-object'); + addGlimmerPackage(vendorPackages, 'glimmer-object-reference'); addGlimmerPackage(vendorPackages, 'glimmer-reference'); addGlimmerPackage(vendorPackages, 'glimmer-runtime'); addGlimmerPackage(vendorPackages, 'glimmer-syntax'); diff --git a/lib/packages.js b/lib/packages.js index 51bf264de8a..aadcf6d8ace 100644 --- a/lib/packages.js +++ b/lib/packages.js @@ -36,6 +36,7 @@ if (glimmerStatus === null || glimmerStatus === true) { ], testingVendorRequirements: [ 'glimmer-object', + 'glimmer-object-reference', 'glimmer-engine-tests' ], hasTemplates: true diff --git a/package.json b/package.json index dca35c906e5..8044f7e217c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "express": "^4.5.0", "finalhandler": "^0.4.0", "github": "^0.2.3", - "glimmer-engine": "tildeio/glimmer#33e6c9a", + "glimmer-engine": "tildeio/glimmer#b184257", "glob": "^5.0.13", "htmlbars": "0.14.16", "mocha": "^2.4.5", diff --git a/packages/ember-glimmer/lib/components/curly-component.js b/packages/ember-glimmer/lib/components/curly-component.js index 753280ee0e9..ea3cf4229ab 100644 --- a/packages/ember-glimmer/lib/components/curly-component.js +++ b/packages/ember-glimmer/lib/components/curly-component.js @@ -1,5 +1,6 @@ import { StatementSyntax, ValueReference } from 'glimmer-runtime'; -import { AttributeBindingReference, applyClassNameBinding } from '../utils/references'; +import { AttributeBindingReference, RootReference, applyClassNameBinding } from '../utils/references'; +import EmptyObject from 'ember-metal/empty_object'; export class CurlyComponentSyntax extends StatementSyntax { constructor({ args, definition, templates }) { @@ -15,13 +16,13 @@ export class CurlyComponentSyntax extends StatementSyntax { } } -function argsToProps(args) { - let attrs = args.named.value(); - let attrKeys = Object.keys(attrs); - let merged = { attrs: {} }; +function attrsToProps(keys, attrs) { + let merged = new EmptyObject(); - for (let i = 0, l = attrKeys.length; i < l; i++) { - let name = attrKeys[i]; + merged.attrs = merged; + + for (let i = 0, l = keys.length; i < l; i++) { + let name = keys[i]; let value = attrs[name]; // Do we have to support passing both class /and/ classNames...? @@ -30,7 +31,6 @@ function argsToProps(args) { } merged[name] = value; - merged.attrs[name] = value; } return merged; @@ -38,23 +38,27 @@ function argsToProps(args) { class CurlyComponentManager { create(definition, args, dynamicScope) { - let klass = definition.ComponentClass; - let component = klass.create(argsToProps(args)); let parentView = dynamicScope.view; + let klass = definition.ComponentClass; + let attrs = args.named.value(); + let props = attrsToProps(args.named.keys, attrs); + + let component = klass.create(props); + dynamicScope.view = component; parentView.appendChild(component); - // component.didInitAttrs({ attrs }); - // component.didReceiveAttrs({ oldAttrs: null, newAttrs: attrs }); - // component.willInsertElement(); - // component.willRender(); + // component.trigger('didInitAttrs', { attrs }); + // component.trigger('didReceiveAttrs', { newAttrs: attrs }); + // component.trigger('willInsertElement'); + // component.trigger('willRender'); return component; } getSelf(component) { - return component; + return new RootReference(component); } didCreateElement(component, element, operations) { @@ -84,26 +88,29 @@ class CurlyComponentManager { } didCreate(component) { - // component.didInsertElement(); - // component.didRender(); + // component.trigger('didInsertElement'); + // component.trigger('didRender'); component._transitionTo('inDOM'); } update(component, args, dynamicScope) { - component.setProperties(argsToProps(args)); + let attrs = args.named.value(); + let props = attrsToProps(args.named.keys, attrs); // let oldAttrs = component.attrs; - // let newAttrs = args.named.value(); - // - // component.didUpdateAttrs({ oldAttrs, newAttrs }); - // component.didReceiveAttrs({ oldAttrs, newAttrs }); - // component.willUpdate(); - // component.willRender(); + // let newAttrs = attrs; + + component.setProperties(props); + + // component.trigger('didUpdateAttrs', { oldAttrs, newAttrs }); + // component.trigger('didReceiveAttrs', { oldAttrs, newAttrs }); + // component.trigger('willUpdate'); + // component.trigger('willRender'); } didUpdate(component) { - // component.didUpdate(); - // component.didRender(); + // component.trigger('didUpdate'); + // component.trigger('didRender'); } getDestructor(component) { diff --git a/packages/ember-glimmer/lib/components/dynamic-component.js b/packages/ember-glimmer/lib/components/dynamic-component.js index 4989faffc7c..58dd886b2f8 100644 --- a/packages/ember-glimmer/lib/components/dynamic-component.js +++ b/packages/ember-glimmer/lib/components/dynamic-component.js @@ -24,10 +24,9 @@ class DynamicComponentReference { constructor({ nameRef, env }) { this.nameRef = nameRef; this.env = env; + this.tag = nameRef.tag; } - isDirty() { return true; } - value() { let { env, nameRef } = this; @@ -39,6 +38,4 @@ class DynamicComponentReference { throw new Error(`Cannot render ${name} as a component`); } } - - destroy() {} } diff --git a/packages/ember-glimmer/lib/components/outlet.js b/packages/ember-glimmer/lib/components/outlet.js index c60a26952e0..784237aae70 100644 --- a/packages/ember-glimmer/lib/components/outlet.js +++ b/packages/ember-glimmer/lib/components/outlet.js @@ -1,5 +1,7 @@ import { StatementSyntax } from 'glimmer-runtime'; +import { ConstReference } from 'glimmer-reference'; import { generateGuid, guidFor } from 'ember-metal/utils'; +import { RootReference, NULL_REFERENCE } from '../utils/references'; export class OutletSyntax extends StatementSyntax { constructor({ args }) { @@ -26,24 +28,13 @@ function outletComponentFor(args, vm) { } } -class TopLevelOutletComponentReference { +class TopLevelOutletComponentReference extends ConstReference { constructor(reference) { - this.reference = reference; - this.definition = null; - } + let outletState = reference.value(); + let definition = new TopLevelOutletComponentDefinition(outletState.render.template); - isDirty() { return true; } - - value() { - if (!this.definition) { - let outletState = this.reference.value(); - this.definition = new TopLevelOutletComponentDefinition(outletState.render.template); - } - - return this.definition; + super(definition); } - - destroy() {} } const INVALIDATE = null; @@ -54,10 +45,9 @@ class OutletComponentReference { this.reference = reference; this.definition = null; this.lastState = null; + this.tag = reference.tag; } - isDirty() { return true; } - value() { let { outletName, reference, definition, lastState } = this; let newState = reference.value(); @@ -100,7 +90,7 @@ class AbstractOutletComponentManager { } getSelf(state) { - return state.render.controller; + return new RootReference(state.render.controller); } getDestructor(state) { @@ -138,7 +128,7 @@ class EmptyOutletComponentManager extends AbstractOutletComponentManager { } getSelf(state) { - return null; + return NULL_REFERENCE; } } diff --git a/packages/ember-glimmer/lib/ember-routing-view/index.js b/packages/ember-glimmer/lib/ember-routing-view/index.js index ee29a4bc3e6..f1f59ac7852 100644 --- a/packages/ember-glimmer/lib/ember-routing-view/index.js +++ b/packages/ember-glimmer/lib/ember-routing-view/index.js @@ -1,5 +1,5 @@ import assign from 'ember-metal/assign'; -import EmptyObject from 'ember-metal/empty_object'; +import { DirtyableTag } from 'glimmer-reference'; /** @module ember @@ -9,7 +9,7 @@ import EmptyObject from 'ember-metal/empty_object'; class OutletStateReference { constructor(outletView) { this.outletView = outletView; - this.children = new EmptyObject(); + this.tag = outletView._tag; } get(key) { @@ -23,20 +23,13 @@ class OutletStateReference { get isTopLevel() { return true; } - - isDirty() { - return true; - } - - destroy() { - } } class ChildOutletStateReference { constructor(parent, key) { this.parent = parent; this.key = key; - this.children = new EmptyObject(); + this.tag = parent.tag; } get(key) { @@ -50,13 +43,6 @@ class ChildOutletStateReference { get isTopLevel() { return false; } - - isDirty() { - return true; - } - - destroy() { - } } export class OutletView { @@ -86,6 +72,7 @@ export class OutletView { this.template = template; this.outletState = null; this._renderResult = null; + this._tag = new DirtyableTag(); } appendTo(selector) { @@ -106,6 +93,7 @@ export class OutletView { setOutletState(state) { this.outletState = state; + this._tag.dirty(); this.rerender(); // FIXME } diff --git a/packages/ember-glimmer/lib/environment.js b/packages/ember-glimmer/lib/environment.js index a2faa45a107..3cd8ceeef70 100644 --- a/packages/ember-glimmer/lib/environment.js +++ b/packages/ember-glimmer/lib/environment.js @@ -1,5 +1,4 @@ import { Environment as GlimmerEnvironment } from 'glimmer-runtime'; -import { isConst } from 'glimmer-reference'; import Dict from 'ember-metal/empty_object'; import { CurlyComponentSyntax, CurlyComponentDefinition } from './components/curly-component'; import { DynamicComponentSyntax } from './components/dynamic-component'; @@ -9,7 +8,6 @@ import createIterable from './utils/iterable'; import { RootReference, ConditionalReference, - ConstConditionalReference, SimpleHelperReference, ClassBasedHelperReference } from './utils/references'; @@ -119,11 +117,14 @@ export default class Environment extends GlimmerEnvironment { } toConditionalReference(reference) { - if (isConst(reference)) { - return new ConstConditionalReference(reference); - } else { - return new ConditionalReference(reference); - } + // if (isConst(reference)) { + // return new ConstConditionalReference(reference); + // } else { + // return new ConditionalReference(reference); + // } + + // FIXME: fix failing proxy tests + return new ConditionalReference(reference); } iterableFor(ref, args) { diff --git a/packages/ember-glimmer/lib/helper.js b/packages/ember-glimmer/lib/helper.js index f21e362e281..7d4081a4eef 100644 --- a/packages/ember-glimmer/lib/helper.js +++ b/packages/ember-glimmer/lib/helper.js @@ -3,7 +3,12 @@ @submodule ember-templates */ +import symbol from 'ember-metal/symbol'; import Object from 'ember-runtime/system/object'; +import { POST_INIT } from 'ember-runtime/system/core_object'; +import { DirtyableTag } from 'glimmer-reference'; + +export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG'); /** Ember Helpers are functions that can compute values, and are used in templates. @@ -48,6 +53,10 @@ import Object from 'ember-runtime/system/object'; var Helper = Object.extend({ isHelperInstance: true, + [POST_INIT]() { + this[RECOMPUTE_TAG] = new DirtyableTag(); + }, + /** On a class-based helper, it may be useful to force a recomputation of that helpers value. This is akin to `rerender` on a component. @@ -72,7 +81,9 @@ var Helper = Object.extend({ @public @since 1.13.0 */ - recompute() {} + recompute() { + this[RECOMPUTE_TAG].dirty(); + } /** Override this function when writing a class-based helper. diff --git a/packages/ember-glimmer/lib/helpers/if-unless.js b/packages/ember-glimmer/lib/helpers/if-unless.js index 3845b77330e..3c0e835cf1a 100644 --- a/packages/ember-glimmer/lib/helpers/if-unless.js +++ b/packages/ember-glimmer/lib/helpers/if-unless.js @@ -3,17 +3,25 @@ @submodule ember-templates */ +import { VOLATILE_TAG } from 'glimmer-reference'; import { assert } from 'ember-metal/debug'; import emberToBool from '../utils/to-bool'; import { InternalHelperReference } from '../utils/references'; +class ConditionalHelperReference extends InternalHelperReference { + constructor(helper, args) { + super(helper, args); + this.tag = VOLATILE_TAG; + } +} + function makeConditionalHelper(toBool) { return { isInternalHelper: true, toReference(args) { switch (args.positional.length) { - case 2: return new InternalHelperReference(conditionalWithoutAlternative, args); - case 3: return new InternalHelperReference(conditionalWithAlternative, args); + case 2: return new ConditionalHelperReference(conditionalWithoutAlternative, args); + case 3: return new ConditionalHelperReference(conditionalWithAlternative, args); default: assert( 'The inline form of the `if` and `unless` helpers expect two or ' + diff --git a/packages/ember-glimmer/lib/utils/references.js b/packages/ember-glimmer/lib/utils/references.js index 3b51960afc5..3ec6dfafebb 100644 --- a/packages/ember-glimmer/lib/utils/references.js +++ b/packages/ember-glimmer/lib/utils/references.js @@ -1,63 +1,114 @@ import { get } from 'ember-metal/property_get'; -import { ConstReference } from 'glimmer-reference'; +import { tagFor } from 'ember-metal/tags'; +import { CURRENT_TAG, CONSTANT_TAG, VOLATILE_TAG, ConstReference, DirtyableTag, UpdatableTag, combine, combineTagged } from 'glimmer-reference'; import { ConditionalReference as GlimmerConditionalReference } from 'glimmer-runtime'; import emberToBool from './to-bool'; +import { RECOMPUTE_TAG } from '../helper'; // @implements PathReference -export class RootReference { - constructor(value) { - this._value = value; +export class PrimitiveReference extends ConstReference { + get() { + return NULL_REFERENCE; + } +} + +export const NULL_REFERENCE = new ConstReference(null); + +// @abstract +// @implements PathReference +class EmberPathReference { + // @abstract get tag() + // @abstract value() + + get(key) { + return new PropertyReference(this, key); + } +} + +// @abstract +class CachedReference extends EmberPathReference { + constructor() { + super(); + this._lastRevision = null; + this._lastValue = null; } value() { - return this._value; + let { tag, _lastRevision, _lastValue } = this; + + if (!_lastRevision || !tag.validate(_lastRevision)) { + this._lastRevision = tag.value(); + _lastValue = this._lastValue = this.compute(); + } + + return _lastValue; } - isDirty() { - return true; + invalidate() { + this._lastRevision = null; } + // @abstract compute() +} + +// @implements PathReference +export class RootReference extends ConstReference { get(propertyKey) { return new PropertyReference(this, propertyKey); } - - destroy() {} } -// @implements PathReference -class PropertyReference { +class PropertyReference extends CachedReference { // jshint ignore:line constructor(parentReference, propertyKey) { + super(); + + let parentReferenceTag = parentReference.tag; + let parentObjectTag = new UpdatableTag(CURRENT_TAG); + + this.tag = combine([parentReferenceTag, parentObjectTag]); this._parentReference = parentReference; + this._parentObjectTag = parentObjectTag; this._propertyKey = propertyKey; } - value() { - return get(this._parentReference.value(), this._propertyKey); - } + compute() { + let { _parentReference, _parentObjectTag, _propertyKey } = this; - isDirty() { - return true; + let parent = _parentReference.value(); + _parentObjectTag.update(tagFor(parent)); + + return get(parent, _propertyKey); } get(propertyKey) { return new PropertyReference(this, propertyKey); } - - destroy() {} } -// @implements PathReference -export class UpdatableReference extends RootReference { +export class UpdatableReference extends EmberPathReference { + constructor(value) { + super(); + + this.tag = new DirtyableTag(); + this._value = value; + } + + value() { + return this._value; + } + update(value) { + this.tag.dirty(); this._value = value; } } // @implements PathReference -export class GetHelperReference { +export class GetHelperReference extends CachedReference { constructor(sourceReference, pathReference) { this.sourceReference = sourceReference; this.pathReference = pathReference; + this.tag = combineTagged([sourceReference, pathReference]); } isDirty() { return true; } @@ -73,26 +124,32 @@ export class GetHelperReference { destroy() {} } -export class HashHelperReference { +export class HashHelperReference extends CachedReference { constructor(args) { + super(); + + this.tag = args.named.tag; this.namedArgs = args.named; } - isDirty() { return true; } - - value() { + compute() { return this.namedArgs.value(); } - - get(propertyKey) { - return this.namedArgs.get(propertyKey); - } - - destroy() {} } export class ConditionalReference extends GlimmerConditionalReference { + constructor(reference) { + super(reference); + + // this.objectTag = new UpdatableTag(CURRENT_TAG); + // this.tag = combine([reference.tag, this.objectTag]); + + // FIXME: fix failing proxy tests + this.tag = VOLATILE_TAG; + } + toBool(predicate) { + // this.objectTag.update(tagFor(predicate)); return emberToBool(predicate); } } @@ -103,76 +160,55 @@ export class ConstConditionalReference extends ConstReference { } } -// @implements PathReference -export class SimpleHelperReference { +export class SimpleHelperReference extends CachedReference { constructor(helper, args) { + super(); + + this.tag = args.tag; this.helper = helper; this.args = args; } - isDirty() { return true; } - - value() { + compute() { let { helper, args: { positional, named } } = this; - return helper(positional.value(), named.value()); } - - get(propertyKey) { - return new PropertyReference(this, propertyKey); - } - - destroy() {} } -// @implements PathReference -export class ClassBasedHelperReference { +export class ClassBasedHelperReference extends CachedReference { constructor(instance, args) { + super(); + + this.tag = combine([instance[RECOMPUTE_TAG], args.tag]); this.instance = instance; this.args = args; } - isDirty() { return true; } - - value() { + compute() { let { instance, args: { positional, named } } = this; - return instance.compute(positional.value(), named.value()); } - - get(propertyKey) { - return new PropertyReference(this, propertyKey); - } - - destroy() {} } -// @implements PathReference -export class InternalHelperReference { +export class InternalHelperReference extends CachedReference { constructor(helper, args) { + super(); + + this.tag = args.tag; this.helper = helper; this.args = args; } - isDirty() { return true; } - - value() { + compute() { let { helper, args } = this; - return helper(args); } - - get(propertyKey) { - return new PropertyReference(this, propertyKey); - } - - destroy() {} } import { assert } from 'ember-metal/debug'; import { dasherize } from 'ember-runtime/system/string'; -export class AttributeBindingReference { +export class AttributeBindingReference extends CachedReference { static apply(component, microsyntax, operations) { let reference = this.parse(component, microsyntax); operations.addAttribute(reference.attributeName, reference); @@ -194,14 +230,23 @@ export class AttributeBindingReference { } } - constructor(component, propertyName, attributeName=propertyName) { + constructor(component, propertyPath, attributeName=propertyPath) { + super(); + + if (propertyPath.indexOf('.') > -1) { + // For bindings like `foo.bar.baz`, just checking the tag for the component itself is not enough. + this.tag = CURRENT_TAG; + } else { + this.tag = tagFor(component); + } + this.component = component; - this.propertyName = propertyName; + this.propertyPath = propertyPath; this.attributeName = attributeName; } - value() { - let value = get(this.component, this.propertyName); + compute() { + let value = get(this.component, this.propertyPath); if (value === null || value === undefined) { return null; @@ -209,9 +254,6 @@ export class AttributeBindingReference { return value; } } - - isDirty() { return true; } - destroy() {} } export function applyClassNameBinding(component, microsyntax, operations) { @@ -227,14 +269,22 @@ export function applyClassNameBinding(component, microsyntax, operations) { operations.addAttribute('class', ref); } -// @implements Reference -class SimpleClassNameBindingReference { +class SimpleClassNameBindingReference extends CachedReference { constructor(component, propertyPath) { + super(); + + if (propertyPath.indexOf('.') > -1) { + // For bindings like `foo.bar.baz`, just checking the tag for the component itself is not enough. + this.tag = CURRENT_TAG; + } else { + this.tag = tagFor(component); + } + this.component = component; this.propertyPath = propertyPath; } - value() { + compute() { let value = get(this.component, this.propertyPath); if (value === true) { @@ -245,27 +295,29 @@ class SimpleClassNameBindingReference { return null; } } - - isDirty() { return true; } - destroy() {} } -// @implements Reference -class ColonClassNameBindingReference { +class ColonClassNameBindingReference extends CachedReference { constructor(component, propertyPath, truthy, falsy) { + super(); + + if (propertyPath.indexOf('.') > -1) { + // For bindings like `foo.bar.baz`, just checking the tag for the component itself is not enough. + this.tag = CURRENT_TAG; + } else { + this.tag = tagFor(component); + } + this.component = component; this.propertyPath = propertyPath; this.truthy = truthy || null; this.falsy = falsy || null; } - value() { + compute() { let value = get(this.component, this.propertyPath); return !!value ? this.truthy : this.falsy; } - - isDirty() { return true; } - destroy() {} } function propertyPathToClassName(propertyPath) { @@ -280,6 +332,7 @@ const EMPTY_OBJECT = {}; // @implements PathReference export class UnboundReference { constructor(sourceRef, key = null) { + this.tag = CONSTANT_TAG; this.sourceRef = sourceRef; this.key = key; this.cache = EMPTY_OBJECT; diff --git a/packages/ember-metal/lib/meta.js b/packages/ember-metal/lib/meta.js index d9eeacf9a1c..2a9a98fe953 100644 --- a/packages/ember-metal/lib/meta.js +++ b/packages/ember-metal/lib/meta.js @@ -27,7 +27,7 @@ import EmptyObject from 'ember-metal/empty_object'; peekBindings, clearBindings, writeValues, peekValues, clearValues, writeDeps, forEachInDeps writableChainWatchers, readableChainWatchers, writableChains, - readableChains + readableChains, writableTag, readableTag */ let members = { @@ -39,7 +39,8 @@ let members = { values: inheritedMap, deps: inheritedMapOfMaps, chainWatchers: ownCustomObject, - chains: inheritedCustomObject + chains: inheritedCustomObject, + tag: ownCustomObject }; let memberNames = Object.keys(members); @@ -55,6 +56,8 @@ function Meta(obj, parentMeta) { this._deps = undefined; this._chainWatchers = undefined; this._chains = undefined; + this._tag = undefined; + // used only internally this.source = obj; diff --git a/packages/ember-metal/lib/property_set.js b/packages/ember-metal/lib/property_set.js index ebf65d1e3d8..cb43c55760b 100644 --- a/packages/ember-metal/lib/property_set.js +++ b/packages/ember-metal/lib/property_set.js @@ -21,6 +21,10 @@ import { toString } from 'ember-metal/utils'; +import { + markObjectAsDirty +} from './tags'; + /** Sets the value of a property on an object, respecting computed properties and notifying observers and other listeners of the change. If the @@ -44,11 +48,13 @@ export function set(obj, keyName, value, tolerant) { assert(`The key provided to set must be a string, you passed ${keyName}`, typeof keyName === 'string'); assert(`'this' in paths is not supported`, !pathHasThis(keyName)); - var meta, possibleDesc, desc; + let meta, possibleDesc, desc; + if (obj) { meta = peekMeta(obj); possibleDesc = obj[keyName]; desc = (possibleDesc !== null && typeof possibleDesc === 'object' && possibleDesc.isDescriptor) ? possibleDesc : undefined; + markObjectAsDirty(meta); } var isUnknown, currentValue; diff --git a/packages/ember-metal/lib/tags.js b/packages/ember-metal/lib/tags.js new file mode 100644 index 00000000000..bff1fd728b4 --- /dev/null +++ b/packages/ember-metal/lib/tags.js @@ -0,0 +1,32 @@ +import { meta as metaFor } from './meta'; +import require, { has } from 'require'; + +let hasGlimmer = has('glimmer-reference'); +let CONSTANT_TAG, CURRENT_TAG, DirtyableTag, makeTag; + +export let markObjectAsDirty; + +export function tagFor(object, _meta) { + if (!hasGlimmer) { + throw new Error('Cannot call tagFor without Glimmer'); + } + + if (object && typeof object === 'object') { + let meta = _meta || metaFor(object); + return meta.writableTag(makeTag); + } else { + return CONSTANT_TAG; + } +} + +if (hasGlimmer) { + ({ DirtyableTag, CONSTANT_TAG, CURRENT_TAG } = require('glimmer-reference')); + makeTag = function() { return new DirtyableTag(); }; + + markObjectAsDirty = function(meta) { + let tag = (meta && meta.readableTag()) || CURRENT_TAG; + tag.dirty(); + }; +} else { + markObjectAsDirty = function() {}; +} diff --git a/packages/ember-runtime/lib/mixins/-proxy.js b/packages/ember-runtime/lib/mixins/-proxy.js index ee880789b31..529d56ad374 100644 --- a/packages/ember-runtime/lib/mixins/-proxy.js +++ b/packages/ember-runtime/lib/mixins/-proxy.js @@ -20,6 +20,13 @@ import { import { computed } from 'ember-metal/computed'; import { defineProperty } from 'ember-metal/properties'; import { Mixin, observer } from 'ember-metal/mixin'; +import symbol from 'ember-metal/symbol'; + +const IS_PROXY = symbol('IS_PROXY'); + +export function isProxy(value) { + return value && value[IS_PROXY]; +} function contentPropertyWillChange(content, contentKey) { var key = contentKey.slice(8); // remove "content." @@ -42,6 +49,8 @@ function contentPropertyDidChange(content, contentKey) { @private */ export default Mixin.create({ + [IS_PROXY]: true, + /** The object whose properties will be forwarded. diff --git a/packages/ember-runtime/lib/mixins/array.js b/packages/ember-runtime/lib/mixins/array.js index 5f96362c588..a2a01b37015 100644 --- a/packages/ember-runtime/lib/mixins/array.js +++ b/packages/ember-runtime/lib/mixins/array.js @@ -27,6 +27,7 @@ import { sendEvent, hasListeners } from 'ember-metal/events'; +import { tagFor } from 'ember-metal/tags'; import EachProxy from 'ember-runtime/system/each_proxy'; function arrayObserversHelper(obj, target, opts, operation, notify) { @@ -484,6 +485,8 @@ export default Mixin.create(Enumerable, { arrayContentDidChange(startIdx, removeAmt, addAmt) { var adding, lim; + tagFor(this).dirty(); + // if no args are passed assume everything changes if (startIdx === undefined) { startIdx = 0;