Skip to content

Commit

Permalink
Merge pull request #2151 from mobxjs/shallow-comparer
Browse files Browse the repository at this point in the history
Shallow comparer
  • Loading branch information
mweststrate authored Oct 12, 2019
2 parents 0502539 + 9005817 commit 92cc3e8
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
* Added `comparer.shallow` for shallow object/array comparisons [#1561](https://github.com/mobxjs/mobx/issues/1561).

# 5.14.0 / 4.14.0

* Added experimental `reactionRequiresObservable` & `observableRequiresReaction` config [#2079](https://github.com/mobxjs/mobx/pull/2079), [Docs](https://github.com/mobxjs/mobx/pull/2082)
Expand Down
2 changes: 1 addition & 1 deletion docs/refguide/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ The expression will automatically be re-evaluated if any observables it uses cha

There are various `options` that can be used to control the behavior of `computed`. These include:

- **`equals: (value, value) => boolean`** Comparison method can be used to override the default detection on when something is changed. Built-in comparers are: `comparer.identity`, `comparer.default`, `comparer.structural`.
- **`equals: (value, value) => boolean`** Comparison method can be used to override the default detection on when something is changed. Built-in comparers are: `comparer.identity`, `comparer.default`, `comparer.structural`, `comparer.shallow`.
- **`name: string`** Provide a debug name to this computed property
- **`requiresReaction: boolean`** Wait for a change in value of the tracked observables, before recomputing the derived property
- **`get: () => value)`** Override the getter for the computed property.
Expand Down
3 changes: 2 additions & 1 deletion docs/refguide/computed-decorator.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ When using `computed` as modifier or as box, it accepts a second options argumen
- `name`: String, the debug name used in spy and the MobX devtools
- `context`: The `this` that should be used in the provided expression
- `set`: The setter function to be used. Without setter it is not possible to assign new values to a computed value. If the second argument passed to `computed` is a function, this is assumed to be a setter.
- `equals`: By default `comparer.default`. This acts as a comparison function for comparing the previous value with the next value. If this function considers the previous and next values to be equal, then observers will not be re-evaluated. This is useful when working with structural data, and types from other libraries. For example, a computed [moment](https://momentjs.com/) instance could use `(a, b) => a.isSame(b)`. `comparer.structural` comes in handy if you want to use structural comparison to determine whether the new value is different from the previous value (and as a result notify observers).
- `equals`: By default `comparer.default`. This acts as a comparison function for comparing the previous value with the next value. If this function considers the previous and next values to be equal, then observers will not be re-evaluated. This is useful when working with structural data, and types from other libraries. For example, a computed [moment](https://momentjs.com/) instance could use `(a, b) => a.isSame(b)`. `comparer.structural` and `comparer.shallow` come in handy if you want to use structural/shallow comparison to determine whether the new value is different from the previous value (and as a result notify observers).
- `requiresReaction`: It is recommended to set this one to `true` on very expensive computed values. If you try to read it's value, but the value is not being tracked by some observer (in which case MobX won't cache the value), it will cause the computed to throw, instead of doing an expensive re-evalution.
- `keepAlive`: don't suspend this computed value if it is not observed by anybody. _Be aware, this can easily lead to memory leaks as it will result in every observable used by this computed value, keeping the computed value in memory!_

Expand All @@ -174,6 +174,7 @@ MobX provides three built-in `comparer`s which should cover most needs:
- `comparer.identity`: Uses the identity (`===`) operator to determine if two values are the same.
- `comparer.default`: The same as `comparer.identity`, but also considers `NaN` to be equal to `NaN`.
- `comparer.structural`: Performs deep structural comparison to determine if two values are the same.
- `comparer.shallow`: Performs shallow structural comparison to determine if two values are the same.

## Computed values run more often than expected

Expand Down
7 changes: 6 additions & 1 deletion src/utils/comparer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ function structuralComparer(a: any, b: any): boolean {
return deepEqual(a, b)
}

function shallowComparer(a: any, b: any): boolean {
return deepEqual(a, b, 1)
}

function defaultComparer(a: any, b: any): boolean {
return Object.is(a, b)
}

export const comparer = {
identity: identityComparer,
structural: structuralComparer,
default: defaultComparer
default: defaultComparer,
shallow: shallowComparer
}
21 changes: 12 additions & 9 deletions src/utils/eq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
declare const Symbol
const toString = Object.prototype.toString

export function deepEqual(a: any, b: any): boolean {
return eq(a, b)
export function deepEqual(a: any, b: any, depth: number = -1): boolean {
return eq(a, b, depth)
}

// Copied from https://github.com/jashkenas/underscore/blob/5c237a7c682fb68fd5378203f0bf22dce1624854/underscore.js#L1186-L1289
// Internal recursive comparison function for `isEqual`.
function eq(a: any, b: any, aStack?: any[], bStack?: any[]) {
function eq(a: any, b: any, depth: number, aStack?: any[], bStack?: any[]) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a === 1 / b
Expand All @@ -26,11 +26,7 @@ function eq(a: any, b: any, aStack?: any[], bStack?: any[]) {
// Exhaust primitive checks
const type = typeof a
if (type !== "function" && type !== "object" && typeof b != "object") return false
return deepEq(a, b, aStack, bStack)
}

// Internal recursive comparison function for `isEqual`.
function deepEq(a: any, b: any, aStack?: any[], bStack?: any[]) {
// Unwrap any wrapped objects.
a = unwrap(a)
b = unwrap(b)
Expand Down Expand Up @@ -84,6 +80,13 @@ function deepEq(a: any, b: any, aStack?: any[], bStack?: any[]) {
return false
}
}

if (depth === 0) {
return false
} else if (depth < 0) {
depth = -1
}

// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

Expand All @@ -109,7 +112,7 @@ function deepEq(a: any, b: any, aStack?: any[], bStack?: any[]) {
if (length !== b.length) return false
// Deep compare the contents, ignoring non-numeric properties.
while (length--) {
if (!eq(a[length], b[length], aStack, bStack)) return false
if (!eq(a[length], b[length], depth - 1, aStack, bStack)) return false
}
} else {
// Deep compare objects.
Expand All @@ -121,7 +124,7 @@ function deepEq(a: any, b: any, aStack?: any[], bStack?: any[]) {
while (length--) {
// Deep compare each member
key = keys[length]
if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false
if (!(has(b, key) && eq(a[key], b[key], depth - 1, aStack, bStack))) return false
}
}
// Remove the first object from the stack of traversed objects.
Expand Down
26 changes: 26 additions & 0 deletions test/base/extras.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,29 @@ test("deepEquals should yield correct results for complex objects #1118 - 2", ()
expect(mobx.comparer.structural(a1, a2)).toBe(true)
expect(mobx.comparer.structural(a1, a4)).toBe(false)
})

test("comparer.shallow should work", () => {
const sh = mobx.comparer.shallow

expect(sh(1, 1)).toBe(true)

expect(sh(1, 2)).toBe(false)

expect(sh({}, {})).toBe(true)
expect(sh([], [])).toBe(true)

expect(sh({}, [])).toBe(false)
expect(sh([], {})).toBe(false)

expect(sh({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true)

expect(sh({ a: 1, b: 2, c: 3, d: 4 }, { a: 1, b: 2, c: 3 })).toBe(false)
expect(sh({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3, d: 4 })).toBe(false)
expect(sh({ a: {}, b: 2, c: 3 }, { a: {}, b: 2, c: 3 })).toBe(false)

expect(sh([1, 2, 3], [1, 2, 3])).toBe(true)

expect(sh([1, 2, 3, 4], [1, 2, 3])).toBe(false)
expect(sh([1, 2, 3], [1, 2, 3, 4])).toBe(false)
expect(sh([{}, 2, 3], [{}, 2, 3])).toBe(false)
})

0 comments on commit 92cc3e8

Please sign in to comment.