Skip to content

Commit

Permalink
[BUGFIX] Adds Arg Proxy feature for tracked properties
Browse files Browse the repository at this point in the history
Currently, we pass a captured snapshot of arguments to the custom
component manager and expect users to update their `args` and trigger
notifications, dirtying any getters that rely on `args`. This is
problematic for a few reasons:

1. It's fundamentally less efficient, since every getter on a component
   will invalidate any time _any_ of the arguments on the component
   changes. This may not be a huge deal in most components, but it could
   be problematic at scale.
2. Upstream components will currently rerender if anything changes
   downstream of them.

The second problem is more of an issue here. The crux of the issue is
that as we are rendering, we must somehow dirty the `args` object in
order for getters to invalidate. Currently, there are two ways of doing
this:

1. Dirtying the tag and bumping the global revision count. This results
   in the component itself being dirtied, which causes arguments to be
   assigned again on the next render, which dirties everything again.
2. Setting the argument's tag to `CURRENT_TAG`, as we do in Glimmer.js.
   This always returns the _latest_ revision though, which means that we
   are _always_ dirty.

This issue can crop up in many forms and leads to infinite rerender bugs
when it does.

The solution proposed here is to instead use a Proxy (or an object with
manually defined getters on platforms which do not support Proxy) to
wrap `args`. This allows us to directly access the references that
represent the arguments, and push their tags onto the autotrack stack as
accessed. Everything entangles properly, and we only rerender or
recalculate a getter when needed.

Alternatively, we could add a way to set the `args` tag to _exactly_
the current revision as the component is rendering. This would solve the
problems of upstream invalidations, but would not solve the issue of all
getters on a component invalidating each render.
  • Loading branch information
Chris Garrett committed Apr 2, 2019
1 parent 0019c1f commit 6de8129
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getCurrentTracker } from '@ember/-internals/metal';
import { Factory } from '@ember/-internals/owner';
import { HAS_NATIVE_PROXY } from '@ember/-internals/utils';
import { OwnedTemplateMeta } from '@ember/-internals/views';
import { EMBER_CUSTOM_COMPONENT_ARG_PROXY } from '@ember/canary-features';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import {
ComponentCapabilities,
Dict,
Expand Down Expand Up @@ -150,16 +154,87 @@ export default class CustomComponentManager<ComponentInstance>
const { delegate } = definition;
const capturedArgs = args.capture();

const component = delegate.createComponent(
definition.ComponentClass.class,
capturedArgs.value()
);
let value;
let namedArgsProxy;

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
if (HAS_NATIVE_PROXY) {
let handler: ProxyHandler<{}> = {
get(_target, prop) {
assert('args can only be strings', typeof prop === 'string');

let tracker = getCurrentTracker();
let ref = capturedArgs.named.get(prop as string);

if (tracker) {
tracker.add(ref.tag);
}

return ref.value();
},
};

if (DEBUG) {
handler.set = function(target, prop) {
assert(
`You attempted to set ${target}#${String(
prop
)} on a components arguments. Component arguments are immutable and cannot be updated directly, they always represent the values that are passed to your component. If you want to set default values, you should use a getter instead`
);

return false;
};
}

namedArgsProxy = new Proxy({}, handler);
} else {
namedArgsProxy = {};
let i;

for (i = 0; i < capturedArgs.named.names.length; i++) {
let name = capturedArgs.named.names[i];
let ref = capturedArgs.named.references[i];

Object.defineProperty(namedArgsProxy, name, {
get() {
let tracker = getCurrentTracker();

if (tracker) {
tracker.add(ref.tag);
}

return ref.value();
},
});
}
}

value = {
named: namedArgsProxy,
positional: capturedArgs.positional.value(),
};
} else {
value = capturedArgs.value();
}

return new CustomComponentState(delegate, component, capturedArgs);
const component = delegate.createComponent(definition.ComponentClass.class, value);

return new CustomComponentState(delegate, component, capturedArgs, namedArgsProxy);
}

update({ delegate, component, args }: CustomComponentState<ComponentInstance>) {
delegate.updateComponent(component, args.value());
update({ delegate, component, args, namedArgsProxy }: CustomComponentState<ComponentInstance>) {
let value;

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
value = {
named: namedArgsProxy!,
positional: args.positional.value(),
};
} else {
value = args.value();
}

delegate.updateComponent(component, value);
}

didCreate({ delegate, component }: CustomComponentState<ComponentInstance>) {
Expand Down Expand Up @@ -221,7 +296,8 @@ export class CustomComponentState<ComponentInstance> {
constructor(
public delegate: ManagerDelegate<ComponentInstance>,
public component: ComponentInstance,
public args: CapturedArguments
public args: CapturedArguments,
public namedArgsProxy?: {}
) {}

destroy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { setComponentManager, capabilities } from '@ember/-internals/glimmer';
import { get, set } from '@ember/-internals/metal';
import { setOwner } from '@ember/-internals/owner';

class GlimmerishComponentManager {
Expand All @@ -12,9 +11,7 @@ class GlimmerishComponentManager {
return new Factory(this.owner, args.named);
}

updateComponent(component, args) {
set(component, 'args', args.named);
}
updateComponent() {}

getContext(component) {
return component;
Expand All @@ -26,14 +23,6 @@ class GlimmerishComponent {
setOwner(this, owner);
this.args = args;
}

get args() {
return get(this, '__args__');
}

set args(args) {
set(this, '__args__', args);
}
}

setComponentManager(() => new GlimmerishComponentManager(), GlimmerishComponent);
Expand Down
4 changes: 4 additions & 0 deletions packages/@ember/canary-features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const DEFAULT_FEATURES = {
EMBER_GLIMMER_ANGLE_BRACKET_NESTED_LOOKUP: true,
EMBER_ROUTING_BUILD_ROUTEINFO_METADATA: true,
EMBER_NATIVE_DECORATOR_SUPPORT: true,
EMBER_CUSTOM_COMPONENT_ARG_PROXY: null,
};

/**
Expand Down Expand Up @@ -84,3 +85,6 @@ export const EMBER_ROUTING_BUILD_ROUTEINFO_METADATA = featureValue(
FEATURES.EMBER_ROUTING_BUILD_ROUTEINFO_METADATA
);
export const EMBER_NATIVE_DECORATOR_SUPPORT = featureValue(FEATURES.EMBER_NATIVE_DECORATOR_SUPPORT);
export const EMBER_CUSTOM_COMPONENT_ARG_PROXY = featureValue(
FEATURES.EMBER_CUSTOM_COMPONENT_ARG_PROXY
);

0 comments on commit 6de8129

Please sign in to comment.