Skip to content

Commit

Permalink
Merge branch 'jamiewinder-computed-comparator', merges #951
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate committed Jul 11, 2017
2 parents 64ba711 + a6b24c1 commit 7ea99bb
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 42 deletions.
26 changes: 17 additions & 9 deletions src/api/autorun.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Lambda, getNextId, invariant, valueDidChange, fail} from "../utils/utils";
import {Lambda, getNextId, invariant, fail} from "../utils/utils";
import {isModifierDescriptor} from "../types/modifiers";
import {Reaction, IReactionPublic, IReactionDisposer} from "../core/reaction";
import {untrackedStart, untrackedEnd} from "../core/derivation";
import {action, isAction} from "./action";
import {IEqualsComparer, comparer} from "../types/comparer";
import {getMessage} from "../utils/messages";

/**
Expand Down Expand Up @@ -153,9 +154,10 @@ export interface IReactionOptions {
context?: any;
fireImmediately?: boolean;
delay?: number;
compareStructural?: boolean;
compareStructural?: boolean; // TODO: remove in 4.0 in favor of equals
/** alias for compareStructural */
struct?: boolean;
struct?: boolean; // TODO: remove in 4.0 in favor of equals
equals?: IEqualsComparer<any>;
name?: string;
}

Expand Down Expand Up @@ -194,7 +196,13 @@ export function reaction<T>(expression: (r: IReactionPublic) => T, effect: (arg:

let firstTime = true;
let isScheduled = false;
let nextValue: T;
let value: T;

const equals = opts.equals
? opts.equals
: (opts.compareStructural || opts.struct)
? comparer.structural
: comparer.default;

const r = new Reaction(opts.name, () => {
if (firstTime || (opts.delay as any) < 1) {
Expand All @@ -213,14 +221,14 @@ export function reaction<T>(expression: (r: IReactionPublic) => T, effect: (arg:
return;
let changed = false;
r.track(() => {
const v = expression(r);
changed = valueDidChange(opts.compareStructural!, nextValue, v);
nextValue = v;
const nextValue = expression(r);
changed = firstTime || !equals(value, nextValue);
value = nextValue;
});
if (firstTime && opts.fireImmediately!)
effect(nextValue, r);
effect(value, r);
if (!firstTime && (changed as boolean) === true)
effect(nextValue, r);
effect(value, r);
if (firstTime)
firstTime = false;
}
Expand Down
26 changes: 18 additions & 8 deletions src/api/computed.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {IEqualsComparer, comparer} from "../types/comparer";
import {asObservableObject, defineComputedProperty} from "../types/observableobject";
import {invariant} from "../utils/utils";
import {createClassPropertyDecorator} from "../utils/decorators";
import {ComputedValue, IComputedValue} from "../core/computedvalue";
import {getMessage} from "../utils/messages";

export interface IComputedValueOptions<T> {
compareStructural?: boolean;
struct?: boolean;
compareStructural?: boolean; // TODO: remove in 4.0 in favor of equals
struct?: boolean; // TODO: remove in 4.0 in favor of equals
equals?: IEqualsComparer<T>;
name?: string;
setter?: (value: T) => void;
context?: any;
Expand All @@ -17,17 +19,17 @@ export interface IComputed {
<T>(func: () => T, options: IComputedValueOptions<T>): IComputedValue<T>;
(target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor): void;
struct(target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor): void;
equals(equals: IEqualsComparer<any>): PropertyDecorator;
}


function createComputedDecorator(compareStructural) {
function createComputedDecorator(equals: IEqualsComparer<any>) {
return createClassPropertyDecorator(
(target, name, _, __, originalDescriptor) => {
invariant(typeof originalDescriptor !== "undefined", getMessage("m009"));
invariant(typeof originalDescriptor.get === "function", getMessage("m010"));

const adm = asObservableObject(target, "");
defineComputedProperty(adm, name, originalDescriptor.get, originalDescriptor.set, compareStructural, false);
defineComputedProperty(adm, name, originalDescriptor.get, originalDescriptor.set, equals, false);
},
function (name) {
const observable = this.$mobx.values[name];
Expand All @@ -43,8 +45,8 @@ function createComputedDecorator(compareStructural) {
);
}

const computedDecorator = createComputedDecorator(false);
const computedStructDecorator = createComputedDecorator(true);
const computedDecorator = createComputedDecorator(comparer.default);
const computedStructDecorator = createComputedDecorator(comparer.structural);

/**
* Decorator for class properties: @computed get value() { return expr; }.
Expand All @@ -59,8 +61,16 @@ export var computed: IComputed = (
invariant(arguments.length < 3, getMessage("m012"));
const opts: IComputedValueOptions<any> = typeof arg2 === "object" ? arg2 : {};
opts.setter = typeof arg2 === "function" ? arg2 : opts.setter;
return new ComputedValue(arg1, opts.context, opts.compareStructural || opts.struct || false, opts.name || arg1.name || "", opts.setter);

const equals = opts.equals
? opts.equals
: (opts.compareStructural || opts.struct)
? comparer.structural
: comparer.default;

return new ComputedValue(arg1, opts.context, equals, opts.name || arg1.name || "", opts.setter);
}
) as any;

computed.struct = computedStructDecorator;
computed.equals = createComputedDecorator;
5 changes: 3 additions & 2 deletions src/api/createtransformer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ComputedValue} from "../core/computedvalue";
import {invariant, getNextId, addHiddenProp} from "../utils/utils";
import {globalState} from "../core/globalstate";
import {comparer} from "../types/comparer";
import {invariant, getNextId, addHiddenProp} from "../utils/utils";

export type ITransformer<A, B> = (object: A) => B;

Expand All @@ -17,7 +18,7 @@ export function createTransformer<A, B>(transformer: ITransformer<A, B>, onClean
// Local transformer class specifically for this transformer
class Transformer extends ComputedValue<B> {
constructor(private sourceIdentifier: string, private sourceObject: A) {
super(() => transformer(sourceObject), undefined, false, `Transformer-${(<any>transformer).name}-${sourceIdentifier}`, undefined);
super(() => transformer(sourceObject), undefined, comparer.default, `Transformer-${(<any>transformer).name}-${sourceIdentifier}`, undefined);
}
onBecomeUnobserved() {
const lastValue = this.value;
Expand Down
21 changes: 14 additions & 7 deletions src/core/computedvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {IObservable, reportObserved, propagateMaybeChanged, propagateChangeConfi
import {IDerivation, IDerivationState, trackDerivedFunction, clearObserving, untrackedStart, untrackedEnd, shouldCompute, CaughtException, isCaughtException} from "./derivation";
import {globalState} from "./globalstate";
import {createAction} from "./action";
import {createInstanceofPredicate, getNextId, valueDidChange, invariant, Lambda, unique, joinStrings, primitiveSymbol, toPrimitive} from "../utils/utils";
import {createInstanceofPredicate, getNextId, invariant, Lambda, unique, joinStrings, primitiveSymbol, toPrimitive} from "../utils/utils";
import {isSpyEnabled, spyReport} from "./spy";
import {autorun} from "../api/autorun";
import {IEqualsComparer} from "../types/comparer";
import {IValueDidChange} from "../types/observablevalue";
import {getMessage} from "../utils/messages";

Expand Down Expand Up @@ -46,7 +47,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
lowestObserverState = IDerivationState.UP_TO_DATE;
unboundDepsCount = 0;
__mapid = "#" + getNextId();
protected value: T | undefined | CaughtException = undefined;
protected value: T | undefined | CaughtException = new CaughtException(null);
name: string;
isComputing: boolean = false; // to check for cycles
isRunningSetter: boolean = false;
Expand All @@ -57,12 +58,14 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
*
* The `name` property is for debug purposes only.
*
* The `compareStructural` property indicates whether the return values should be compared structurally.
* Normally, a computed value will not notify an upstream observer if a newly produced value is strictly equal to the previously produced value.
* However, enabling compareStructural can be convenient if you always produce an new aggregated object and don't want to notify observers if it is structurally the same.
* The `equals` property specifies the comparer function to use to determine if a newly produced
* value differs from the previous value. Two comparers are provided in the library; `defaultComparer`
* compares based on identity comparison (===), and `structualComparer` deeply compares the structure.
* Structural comparison can be convenient if you always produce an new aggregated object and
* don't want to notify observers if it is structurally the same.
* This is useful for working with vectors, mouse coordinates etc.
*/
constructor(public derivation: () => T, public scope: Object | undefined, private compareStructural: boolean, name: string, setter?: (v: T) => void) {
constructor(public derivation: () => T, public scope: Object | undefined, private equals: IEqualsComparer<any>, name: string, setter?: (v: T) => void) {
this.name = name || "ComputedValue@" + getNextId();
if (setter)
this.setter = createAction(name + "-setter", setter) as any;
Expand Down Expand Up @@ -135,7 +138,11 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
}
const oldValue = this.value;
const newValue = this.value = this.computeValue(true);
return isCaughtException(newValue) || valueDidChange(this.compareStructural, newValue, oldValue);
return (
isCaughtException(oldValue) ||
isCaughtException(newValue) ||
!this.equals(oldValue, newValue)
);
}

computeValue(track: boolean) {
Expand Down
7 changes: 5 additions & 2 deletions src/mobx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { useStrict, isStrictModeEnabled, IAction } from "./core/act
export { spy } from "./core/spy";
export { IComputedValue } from "./core/computedvalue";

export { IEqualsComparer, comparer } from "./types/comparer";
export { asReference, asFlat, asStructure, asMap } from "./types/modifiers-old";
export { IModifierDescriptor, IEnhancer, isModifierDescriptor } from "./types/modifiers";
export { IInterceptable, IInterceptor } from "./types/intercept-utils";
Expand Down Expand Up @@ -74,8 +75,8 @@ import { isComputingDerivation } from "./core/derivation";
import { setReactionScheduler, onReactionError } from "./core/reaction";
import { reserveArrayBuffer, IObservableArray } from "./types/observablearray";
import { interceptReads } from "./api/intercept-read";
import { ObservableMap } from './types/observablemap';
import { IObservableValue } from './types/observablevalue';
import { ObservableMap } from "./types/observablemap";
import { IObservableValue } from "./types/observablevalue";
import {registerGlobals} from "./core/globalstate";

// This line should come after all the imports as well, for the same reason
Expand Down Expand Up @@ -115,6 +116,7 @@ import { IAtom, Atom, BaseAtom } from "./core/ato
import { useStrict, isStrictModeEnabled, IAction } from "./core/action";
import { spy } from "./core/spy";
import { IComputedValue } from "./core/computedvalue";
import { IEqualsComparer, comparer } from "./types/comparer";
import { asReference, asFlat, asStructure, asMap } from "./types/modifiers-old";
import { IModifierDescriptor, IEnhancer, isModifierDescriptor } from "./types/modifiers";
import { IInterceptable, IInterceptor } from "./types/intercept-utils";
Expand Down Expand Up @@ -146,6 +148,7 @@ const everything = {
Atom, BaseAtom,
useStrict, isStrictModeEnabled,
spy,
comparer,
asReference, asFlat, asStructure, asMap,
isModifierDescriptor,
isObservableObject,
Expand Down
29 changes: 29 additions & 0 deletions src/types/comparer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { deepEqual } from '../utils/utils';

export interface IEqualsComparer<T> {
(a: T, b: T): boolean;
}

function identityComparer(a: any, b: any): boolean {
return a === b;
}

function structuralComparer(a: any, b: any): boolean {
if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) {
return true;
}
return deepEqual(a, b);
}

function defaultComparer(a: any, b: any): boolean {
if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) {
return true;
}
return identityComparer(a, b);
}

export const comparer = {
identity: identityComparer,
structural: structuralComparer,
default: defaultComparer
};
8 changes: 4 additions & 4 deletions src/types/observableobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {runLazyInitializers} from "../utils/decorators";
import {hasInterceptors, IInterceptable, registerInterceptor, interceptChange} from "./intercept-utils";
import {IListenable, registerListener, hasListeners, notifyListeners} from "./listen-utils";
import {isSpyEnabled, spyReportStart, spyReportEnd} from "../core/spy";
import {IEqualsComparer, comparer} from "./comparer";
import {IEnhancer, isModifierDescriptor, IModifierDescriptor} from "./modifiers";
import {isAction, defineBoundAction} from "../api/action";
import {getMessage} from "../utils/messages";



export interface IObservableObject {
"observable-object": IObservableObject;
}
Expand Down Expand Up @@ -100,7 +100,7 @@ export function defineObservablePropertyFromDescriptor(adm: ObservableObjectAdmi
}
} else {
// get x() { return 3 } set x(v) { }
defineComputedProperty(adm, propName, descriptor.get, descriptor.set, false, true);
defineComputedProperty(adm, propName, descriptor.get, descriptor.set, comparer.default, true);
}
}

Expand Down Expand Up @@ -135,13 +135,13 @@ export function defineComputedProperty(
propName: string,
getter,
setter,
compareStructural: boolean,
equals: IEqualsComparer<any>,
asInstanceProperty: boolean
) {
if (asInstanceProperty)
assertPropertyConfigurable(adm.target, propName);

adm.values[propName] = new ComputedValue(getter, adm.target, compareStructural, `${adm.name}.${propName}`, setter);
adm.values[propName] = new ComputedValue(getter, adm.target, equals, `${adm.name}.${propName}`, setter);
if (asInstanceProperty) {
Object.defineProperty(adm.target, propName, generateComputedPropConfig(propName));
}
Expand Down
9 changes: 0 additions & 9 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,6 @@ export function objectAssign() {
return res;
}

export function valueDidChange(compareStructural: boolean, oldValue, newValue): boolean {
if (typeof oldValue === 'number' && isNaN(oldValue)) {
return typeof newValue !== 'number' || !isNaN(newValue);
}
return compareStructural
? !deepEqual(oldValue, newValue)
: oldValue !== newValue;
}

const prototypeHasOwnProperty = Object.prototype.hasOwnProperty;
export function hasOwnProperty(object: Object, propName: string) {
return prototypeHasOwnProperty.call(object, propName);
Expand Down
1 change: 1 addition & 0 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test('correct api should be exposed', function(t) {
'autorun',
'autorunAsync',
'computed',
'comparer',
'createTransformer',
'default',
'expr',
Expand Down
35 changes: 35 additions & 0 deletions test/babel/babel-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -873,3 +873,38 @@ test("action.bound binds (Babel)", t=> {

t.end();
})

test("@computed.equals (Babel)", t => {
const sameTime = (from, to) => from.hour === to.hour && from.minute === to.minute;
class Time {
constructor(hour, minute) {
this.hour = hour;
this.minute = minute;
}

@observable hour: number;
@observable minute: number;

@computed.equals(sameTime) get time() {
return { hour: this.hour, minute: this.minute };
}
}
const time = new Time(9, 0);

const changes = [];
const disposeAutorun = autorun(() => changes.push(time.time));

t.deepEqual(changes, [ { hour: 9, minute: 0 }]);
time.hour = 9;
t.deepEqual(changes, [ { hour: 9, minute: 0 }]);
time.minute = 0;
t.deepEqual(changes, [ { hour: 9, minute: 0 }]);
time.hour = 10;
t.deepEqual(changes, [ { hour: 9, minute: 0 }, { hour: 10, minute: 0 }]);
time.minute = 30;
t.deepEqual(changes, [ { hour: 9, minute: 0 }, { hour: 10, minute: 0 }, { hour: 10, minute: 30 }]);

disposeAutorun();

t.end();
});
Loading

0 comments on commit 7ea99bb

Please sign in to comment.