Skip to content

Commit

Permalink
Faster throw handling (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexreardon authored Aug 31, 2018
1 parent 78b6e96 commit f05791e
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 32 deletions.
50 changes: 32 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,40 +175,54 @@ Therefore, in order to prevent against unexpected results, `memoize-one` takes i

Generally this will be of no impact if you are not explicity controlling the `this` context of functions you want to memoize with [explicit binding](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#explicit-binding) or [implicit binding](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#implicit-binding). `memoize-One` will detect when you are manipulating `this` and will then consider the `this` context as an argument. If `this` changes, it will re-execute the original function even if the arguments have not changed.

## Exceptions
## When your result function `throw`s

> There is no caching when your result function throws
If your result function `throw`s then we will we will not cache the thrown result. If the memoized function is next called with the same arguments then we will re-execute the memoized function.
If your result function `throw`s then the memoized function will also throw. The throw will not break the memoized functions existing argument cache. It means the memoized function will pretend like it was never called with arguments that made it `throw`.

```js
const willThrow = (message) => {
console.log(message);
throw new Error(message);
const canThrow = (name: string) => {
console.log('called');
if(name === 'throw') {
throw new Error(name);
}
return { name };
}

const memoized = memoizeOne(willThrow);
let firstError;
let secondError;
const memoized = memoizeOne(canThrow);

const value1 = memoized('Alex');
// console.log => 'called'
const value2 = memoized('Alex');
// result function not called

console.log(value1 === value2);
// console.log => true

try {
memoized('first message');
// console.log => 'first message'
} catch (e) {
memoized('throw');
// console.log => 'called'
} catch(e) {
firstError = e;
}

try {
memoized('first message');
// console.log => 'first message'
// even though the arguments are the same the result function was called again
} catch (e) {
memoized('throw');
// console.log => 'called'
// the result function was called again even though it was called twice
// with the 'throw' string
} catch(e) {
secondError = e;
}

// error has a new reference as the function was called again
console.log(firstError === secondError);
// console.log => false
console.log(firstError !== secondError);


const value3 = memoized('Alex');
// result function not called as the original memoization cache has not been busted
console.log(value1 === value3);
// console.log => true
```

## Performance :rocket:
Expand Down
16 changes: 5 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,26 @@ export default function <ResultFn: (...Array<any>) => mixed>(resultFn: ResultFn,
let lastArgs: Array<mixed> = [];
let lastResult: mixed;
let calledOnce: boolean = false;
let lastThrew: boolean = false;

const isNewArgEqualToLast = (newArg: mixed, index: number): boolean => isEqual(newArg, lastArgs[index]);

// breaking cache when context (this) or arguments change
const result = function (...newArgs: Array<mixed>) {
if (calledOnce &&
!lastThrew &&
lastThis === this &&
newArgs.length === lastArgs.length &&
newArgs.every(isNewArgEqualToLast)) {
return lastResult;
}

// Throwing during an assignment aborts the assignment: https://codepen.io/alexreardon/pen/RYKoaz
// Doing the lastResult assignment first so that if it throws
// nothing will be overwritten
lastResult = resultFn.apply(this, newArgs);
calledOnce = true;
lastThrew = false;
lastThis = this;
lastArgs = newArgs;

try {
lastResult = resultFn.apply(this, newArgs);
return lastResult;
} catch (e) {
lastThrew = true;
throw e;
}
return lastResult;
};

// telling flow to ignore the type of `result` as we know it is `ResultFn`
Expand Down
6 changes: 3 additions & 3 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,11 +557,11 @@ describe('memoizeOne', () => {
expect(firstError).toEqual(secondError);
expect(firstError).not.toBe(secondError);

// last successful cache value is lost
// last successful cache value is not lost
const result3 = memoized(false);
expect(canThrow).toHaveBeenCalledTimes(4);
expect(canThrow).toHaveBeenCalledTimes(3);
// new result
expect(result3).not.toBe(result2);
expect(result3).toBe(result2);
});

it('should throw regardless of the type of the thrown value', () => {
Expand Down

0 comments on commit f05791e

Please sign in to comment.