From f46e06ee8aaaf22ed983b839999c3b4030049532 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 16 Feb 2018 21:51:56 -0800 Subject: [PATCH 1/2] Autotrack, take 2 --- packages/@glimmer/component/src/component.ts | 3 +- packages/@glimmer/component/src/tracked.ts | 113 ++++++++++++++---- .../test/browser/tracked-property-test.ts | 88 +++++++++++++- 3 files changed, 175 insertions(+), 29 deletions(-) diff --git a/packages/@glimmer/component/src/component.ts b/packages/@glimmer/component/src/component.ts index 234b56ce6..415286c5f 100644 --- a/packages/@glimmer/component/src/component.ts +++ b/packages/@glimmer/component/src/component.ts @@ -1,5 +1,6 @@ import { assert } from "@glimmer/util"; import { metaFor } from "./tracked"; +import { CURRENT_TAG } from "@glimmer/reference"; export interface Bounds { firstNode: Node; @@ -266,7 +267,7 @@ class Component { set args(args) { this.__args__ = args; - metaFor(this).dirtyableTagFor("args").inner.dirty(); + metaFor(this).updatableTagFor("args").inner.update(CURRENT_TAG); } /** @private diff --git a/packages/@glimmer/component/src/tracked.ts b/packages/@glimmer/component/src/tracked.ts index 70897109a..6371d1cc9 100644 --- a/packages/@glimmer/component/src/tracked.ts +++ b/packages/@glimmer/component/src/tracked.ts @@ -1,6 +1,23 @@ import { DEBUG } from "@glimmer/env"; -import { Tag, DirtyableTag, TagWrapper, combine, CONSTANT_TAG } from "@glimmer/reference"; -import { dict, Dict } from "@glimmer/util"; +import { Tag, DirtyableTag, UpdatableTag, TagWrapper, combine, CONSTANT_TAG, CURRENT_TAG } from "@glimmer/reference"; +import { dict, Dict, Option } from "@glimmer/util"; + +/** + * An object that that tracks @tracked properties that were consumed. + */ +class Tracker { + private tags = new Set(); + + add(tag: Tag) { + this.tags.add(tag); + } + + combine(): Tag { + let tags: Tag[] = []; + this.tags.forEach(tag => tags.push(tag)); + return combine(tags); + } +} /** * @decorator @@ -82,19 +99,62 @@ export function tracked(...dependencies: any[]): any { } } +/** + * Whenever a tracked computed property is entered, the current tracker is + * saved off and a new tracker is replaced. + * + * Any tracked properties consumed are added to the current tracker. + * + * When a tracked computed property is exited, the tracker's tags are + * combined and added to the parent tracker. + * + * The consequence is that each tracked computed property has a tag + * that corresponds to the tracked properties consumed inside of + * itself, including child tracked computed properties. + */ +let CURRENT_TRACKER: Option = null; + function descriptorForTrackedComputedProperty(target: any, key: any, descriptor: PropertyDescriptor, dependencies: string[]): PropertyDescriptor { let meta = metaFor(target); meta.trackedProperties[key] = true; meta.trackedPropertyDependencies[key] = dependencies || []; + let get = descriptor.get as Function; + let set = descriptor.set as Function; + + function getter(this: any) { + // Swap the parent tracker for a new tracker + let old = CURRENT_TRACKER; + let tracker = CURRENT_TRACKER = new Tracker(); + + // Call the getter + let ret = get.call(this); + + // Swap back the parent tracker + CURRENT_TRACKER = old; + + // Combine the tags in the new tracker and add them to the parent tracker + let tag = tracker.combine(); + if (CURRENT_TRACKER) CURRENT_TRACKER.add(tag); + + // Update the UpdatableTag for this property with the tag for all of the + // consumed dependencies. + metaFor(this).updatableTagFor(key).inner.update(tag); + + return ret; + } + return { enumerable: true, configurable: false, - get: descriptor.get, + get: getter, set: function() { - metaFor(this).dirtyableTagFor(key).inner.dirty(); - descriptor.set.apply(this, arguments); - propertyDidChange(); + // Bump the global revision counter + EPOCH.inner.dirty(); + + // Mark the UpdatableTag for this property with the current tag. + metaFor(this).updatableTagFor(key).inner.update(CURRENT_TAG); + set.apply(this, arguments); } }; } @@ -125,11 +185,16 @@ function installTrackedProperty(target: any, key: Key) { configurable: true, get() { + if (CURRENT_TRACKER) CURRENT_TRACKER.add(metaFor(this).updatableTagFor(key)); return this[shadowKey]; }, set(newValue) { - metaFor(this).dirtyableTagFor(key).inner.dirty(); + // Bump the global revision counter + EPOCH.inner.dirty(); + + // Mark the UpdatableTag for this property with the current tag. + metaFor(this).updatableTagFor(key).inner.update(CURRENT_TAG); this[shadowKey] = newValue; propertyDidChange(); } @@ -152,13 +217,13 @@ function installTrackedProperty(target: any, key: Key) { */ export default class Meta { tags: Dict; - computedPropertyTags: Dict>; + computedPropertyTags: Dict>; trackedProperties: Dict; trackedPropertyDependencies: Dict; constructor(parent: Meta) { this.tags = dict(); - this.computedPropertyTags = dict>(); + this.computedPropertyTags = dict>(); this.trackedProperties = parent ? Object.create(parent.trackedProperties) : dict(); this.trackedPropertyDependencies = parent ? Object.create(parent.trackedPropertyDependencies) : dict(); } @@ -168,8 +233,8 @@ export default class Meta { * by e.g. Glimmer VM to detect when a property should be re-rendered. Think * of this as the "public-facing" tag. * - * For static tracked properties, this is a single DirtyableTag. For computed - * properties, it is a combinator of the property's DirtyableTag as well as + * For static tracked properties, this is a single UpdatableTag. For computed + * properties, it is a combinator of the property's UpdatableTag as well as * all of its dependencies' tags. */ tagFor(key: Key): Tag { @@ -186,11 +251,11 @@ export default class Meta { /** * The tag used internally to invalidate when a tracked property is set. For - * static properties, this is the same DirtyableTag returned from `tagFor`. - * For computed properties, it is the DirtyableTag used as one of the tags in + * static properties, this is the same UpdatableTag returned from `tagFor`. + * For computed properties, it is the UpdatableTag used as one of the tags in * the tag combinator of the CP and its dependencies. */ - dirtyableTagFor(key: Key): TagWrapper { + updatableTagFor(key: Key): TagWrapper { let dependencies = this.trackedPropertyDependencies[key]; let tag; @@ -198,19 +263,19 @@ export default class Meta { // The key is for a computed property. tag = this.computedPropertyTags[key]; if (tag) { return tag; } - return this.computedPropertyTags[key] = DirtyableTag.create(); + return this.computedPropertyTags[key] = UpdatableTag.create(CURRENT_TAG); } else { // The key is for a static property. tag = this.tags[key]; - if (tag) { return tag as TagWrapper; } - return this.tags[key] = DirtyableTag.create(); + if (tag) { return tag as TagWrapper; } + return this.tags[key] = UpdatableTag.create(CURRENT_TAG); } } } function combinatorForComputedProperties(meta: Meta, key: Key, dependencies: Key[] | void): Tag { // Start off with the tag for the CP's own dirty state. - let tags: Tag[] = [meta.dirtyableTagFor(key)]; + let tags: Tag[] = [meta.updatableTagFor(key)]; // Next, add in all of the tags for its dependencies. if (dependencies && dependencies.length) { @@ -243,6 +308,8 @@ function hasOwnProperty(obj: any, key: symbol) { return hOP.call(obj, key); } +const EPOCH = DirtyableTag.create(); + let propertyDidChange = function() {}; export function setPropertyDidChange(cb: () => void) { @@ -300,7 +367,7 @@ export function tagForProperty(obj: any, key: string, throwError: UntrackedPrope */ function installDevModeErrorInterceptor(obj: object, key: string, throwError: UntrackedPropertyErrorThrower) { let target = obj; - let descriptor: PropertyDescriptor; + let descriptor: Option = null; // Find the descriptor for the current property. We may need to walk the // prototype chain to do so. If the property is undefined, we may never get a @@ -316,16 +383,18 @@ function installDevModeErrorInterceptor(obj: object, key: string, throwError: Un // If possible, define a property descriptor that passes through the current // value on reads but throws an exception on writes. if (descriptor) { + let { get, value } = descriptor; + if (descriptor.configurable || !hasOwnDescriptor) { Object.defineProperty(obj, key, { configurable: descriptor.configurable, enumerable: descriptor.enumerable, get() { - if (descriptor.get) { - return descriptor.get.call(this); + if (get) { + return get.call(this); } else { - return descriptor.value; + return value; } }, diff --git a/packages/@glimmer/component/test/browser/tracked-property-test.ts b/packages/@glimmer/component/test/browser/tracked-property-test.ts index 642d7c524..7b196792f 100644 --- a/packages/@glimmer/component/test/browser/tracked-property-test.ts +++ b/packages/@glimmer/component/test/browser/tracked-property-test.ts @@ -172,15 +172,14 @@ test('can track a computed property', (assert) => { test('tracked computed properties are invalidated when their dependencies are invalidated', (assert) => { class TrackedPerson { - @tracked('fullName') - get salutation() { + @tracked get salutation() { return `Hello, ${this.fullName}!`; } - @tracked('firstName', 'lastName') - get fullName() { + @tracked get fullName() { return `${this.firstName} ${this.lastName}`; } + set fullName(fullName) { let [firstName, lastName] = fullName.split(' '); this.firstName = firstName; @@ -192,8 +191,8 @@ test('tracked computed properties are invalidated when their dependencies are in } let obj = new TrackedPerson(); - assert.strictEqual(obj.salutation, 'Hello, Tom Dale!'); - assert.strictEqual(obj.fullName, 'Tom Dale'); + assert.strictEqual(obj.salutation, 'Hello, Tom Dale!', `the saluation field is valid`); + assert.strictEqual(obj.fullName, 'Tom Dale', `the fullName field is valid`); let tag = tagForProperty(obj, 'salutation'); let snapshot = tag.value(); @@ -219,6 +218,83 @@ test('tracked computed properties are invalidated when their dependencies are in assert.strictEqual(tag.validate(snapshot), true); }); +test('nested @tracked in multiple objects', (assert) => { + class TrackedPerson { + @tracked get salutation() { + return `Hello, ${this.fullName}!`; + } + + @tracked get fullName(): string { + return `${this.firstName} ${this.lastName}`; + } + + set fullName(fullName: string) { + let [firstName, lastName] = fullName.split(' '); + this.firstName = firstName; + this.lastName = lastName; + } + + toString() { + return this.fullName; + } + + @tracked firstName = 'Tom'; + @tracked lastName = 'Dale'; + } + + class TrackedContact { + @tracked email: string; + @tracked person: TrackedPerson; + + constructor(person: TrackedPerson, email: string) { + this.person = person; + this.email = email; + } + + @tracked get contact(): string { + return `${this.person} @ ${this.email}`; + } + } + + let obj = new TrackedContact(new TrackedPerson(), 'tom@example.com'); + assert.strictEqual(obj.contact, 'Tom Dale @ tom@example.com', `the contact field is valid`); + assert.strictEqual(obj.person.fullName, 'Tom Dale', `the fullName field is valid`); + let person = obj.person; + + let tag = tagForProperty(obj, 'contact'); + let snapshot = tag.value(); + assert.ok(tag.validate(snapshot), 'tag should be valid to start'); + + person.firstName = 'Edsger'; + person.lastName = 'Dijkstra'; + assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after nested dependency is set'); + assert.strictEqual(person.fullName, 'Edsger Dijkstra'); + assert.strictEqual(obj.contact, 'Edsger Dijkstra @ tom@example.com'); + + snapshot = tag.value(); + assert.strictEqual(tag.validate(snapshot), true); + + person.fullName = 'Alan Kay'; + assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after chained dependency is set'); + assert.strictEqual(person.fullName, 'Alan Kay'); + assert.strictEqual(person.firstName, 'Alan'); + assert.strictEqual(person.lastName, 'Kay'); + assert.strictEqual(obj.contact, 'Alan Kay @ tom@example.com'); + + snapshot = tag.value(); + assert.strictEqual(tag.validate(snapshot), true); + + obj.email = "alan@example.com"; + assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after chained dependency is set'); + assert.strictEqual(person.fullName, 'Alan Kay'); + assert.strictEqual(person.firstName, 'Alan'); + assert.strictEqual(person.lastName, 'Kay'); + assert.strictEqual(obj.contact, 'Alan Kay @ alan@example.com'); + + snapshot = tag.value(); + assert.strictEqual(tag.validate(snapshot), true); +}); + module('[@glimmer/component] Tracked Property Warning in Development Mode'); if (DEBUG) { From 4fd3c7c90fdda01b2c9e4c6906ab15c7f1bcf67d Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 21 Feb 2018 09:58:34 -0500 Subject: [PATCH 2/2] Fix linting issue: trailing whitespace --- packages/@glimmer/component/src/tracked.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@glimmer/component/src/tracked.ts b/packages/@glimmer/component/src/tracked.ts index 6371d1cc9..41733fd61 100644 --- a/packages/@glimmer/component/src/tracked.ts +++ b/packages/@glimmer/component/src/tracked.ts @@ -102,12 +102,12 @@ export function tracked(...dependencies: any[]): any { /** * Whenever a tracked computed property is entered, the current tracker is * saved off and a new tracker is replaced. - * + * * Any tracked properties consumed are added to the current tracker. - * + * * When a tracked computed property is exited, the tracker's tags are * combined and added to the parent tracker. - * + * * The consequence is that each tracked computed property has a tag * that corresponds to the tracked properties consumed inside of * itself, including child tracked computed properties.