memoize-one
Advanced tools
Comparing version 1.0.0-rc.1 to 1.0.0
@@ -10,2 +10,3 @@ "use strict"; | ||
var lastThis = void 0; | ||
var lastArgs = []; | ||
@@ -15,2 +16,3 @@ var lastResult = void 0; | ||
// breaking cache when arguments or context changes | ||
return function () { | ||
@@ -21,3 +23,3 @@ for (var _len = arguments.length, newArgs = Array(_len), _key = 0; _key < _len; _key++) { | ||
if (calledOnce && newArgs.length === lastArgs.length && lastArgs.every(function (lastArg, i) { | ||
if (calledOnce && lastThis === this && newArgs.length === lastArgs.length && lastArgs.every(function (lastArg, i) { | ||
return isEqual(lastArg, newArgs[i]); | ||
@@ -29,2 +31,3 @@ })) { | ||
calledOnce = true; | ||
lastThis = this; | ||
lastArgs = newArgs; | ||
@@ -31,0 +34,0 @@ lastResult = resultFn.apply(this, newArgs); |
{ | ||
"name": "memoize-one", | ||
"version": "1.0.0-rc.1", | ||
"description": "A memoization library for memoizing a function with a cache size of one", | ||
"version": "1.0.0", | ||
"description": "A memoization library which only remembers the latest invokation", | ||
"main": "lib/index.js", | ||
@@ -47,3 +47,8 @@ "module": "src/index.js", | ||
"prepublish": "yarn run build" | ||
} | ||
}, | ||
"keywords": [ | ||
"memoize", | ||
"cache", | ||
"performance" | ||
] | ||
} |
# memoizeOne | ||
A simple memoization library which only remembers the latest invokation | ||
A memoization library which only remembers the latest invokation | ||
[![Build Status](https://travis-ci.org/alexreardon/memoize-one.svg?branch=master)](https://travis-ci.org/alexreardon/memoize-one) | ||
[![codecov](https://codecov.io/gh/alexreardon/memoize-one/branch/master/graph/badge.svg)](https://codecov.io/gh/alexreardon/memoize-one) | ||
[![Build Status](https://travis-ci.org/alexreardon/memoize-one.svg?branch=master)](https://travis-ci.org/alexreardon/memoize-one) [![codecov](https://codecov.io/gh/alexreardon/memoize-one/branch/master/graph/badge.svg)](https://codecov.io/gh/alexreardon/memoize-one) [![dependencies](https://david-dm.org/alexreardon/memoize-one.svg)](https://david-dm.org/alexreardon/memoize-one) [![SemVer](https://img.shields.io/badge/SemVer-2.0.0-brightgreen.svg)](http://semver.org/spec/v2.0.0.html) | ||
## DOCS: Work in progress | ||
## Rationale | ||
@@ -20,5 +17,4 @@ | ||
Unlike other memoization libraries, `memoizeOne` only remembers the latest arguments. No need to worry about cache busting mechanisms such as `maxAge`, `maxSize`, `exlusions` and so on which can be prone to memory leaks. `memoizeOne` simply remembers the last arguments, and if the function is next called with the same arguments then it returns the previous result. | ||
Unlike other memoization libraries, `memoizeOne` only remembers the latest arguments and result. No need to worry about cache busting mechanisms such as `maxAge`, `maxSize`, `exlusions` and so on which can be prone to memory leaks. `memoizeOne` simply remembers the last arguments, and if the function is next called with the same arguments then it returns the previous result. | ||
## Usage | ||
@@ -50,2 +46,3 @@ | ||
``` | ||
[Play with this example](http://www.webpackbin.com/NkCiYkz_M) | ||
@@ -74,3 +71,11 @@ ### Custom equality function | ||
``` | ||
[Play with this example](http://www.webpackbin.com/NJW-tJMdf) | ||
#### Type signature | ||
Here is the expected [flow](http://flowtype.org) type signature for a custom equality function: | ||
```js | ||
type EqualityFn = (a: any, b: any) => boolean; | ||
``` | ||
## Installation | ||
@@ -86,13 +91,46 @@ | ||
## Other features | ||
## `this` | ||
### Correctly supports `this` binding | ||
### memoizeOne correctly respects `this` control | ||
### Custom equality function | ||
This library takes special care to maintain, and allow control over the the `this` context for **both** the original function being memoized as well as the returned memoized function. Both the original function and the memoized function's `this` context respect [all the `this` controlling techniques](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md): | ||
### Code health | ||
- new bindings (`new`) | ||
- explicit binding (`call`, `apply`, `bind`); | ||
- implicit binding (call site: `obj.foo()`); | ||
- default binding (`window` or `undefined` in `strict mode`); | ||
- fat arrow binding (binding to lexical `this`) | ||
- ignored this (pass `null` as `this` to explicit binding) | ||
- Tested with [all JavaScript *types*](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md) | ||
- 100% code coverage | ||
- [flow types](http://flowtype.org) for safer internal execution and type checking / auto complete for editors | ||
- [Semantically versioning (2.0)](http://semver.org/) | ||
### Changes to `this` is considered an argument change | ||
Changes to the running context (`this`) of a function can result in the function returning a different value event though its arguments have stayed the same: | ||
```js | ||
function getA() { | ||
return this.a; | ||
} | ||
const temp1 = { | ||
a: 20, | ||
}; | ||
const temp2 = { | ||
a: 30, | ||
} | ||
getA.call(temp1); // 20 | ||
getA.call(temp2); // 30 | ||
``` | ||
Therefore, in order to prevent against unexpected results, `memoizeOne` takes into account the current execution context (`this`) of the memoized function. If `this` is different to the previous invokation then it is considered a change in argument. [further discussion](https://github.com/alexreardon/memoize-one/issues/3). | ||
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). `memoizeOne` 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. | ||
## Code health | ||
- Tested with all built in [JavaScript types](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md). | ||
- 100% code coverage. | ||
- [flow types](http://flowtype.org) for safer internal execution and external consumption. Also allows for editor autocompletion. | ||
- Follows [Semantic versioning (2.0)](http://semver.org/) for safer versioning. | ||
- Lightweight with no dependencies |
@@ -7,2 +7,3 @@ // @flow | ||
export default function (resultFn: Function, isEqual?: EqualityFn = simpleIsEqual) { | ||
let lastThis: any; | ||
let lastArgs: Array<any> = []; | ||
@@ -12,4 +13,7 @@ let lastResult: any; | ||
// breaking cache when arguments or context changes | ||
return function (...newArgs: Array<any>) { | ||
if (calledOnce && newArgs.length === lastArgs.length && | ||
if (calledOnce && | ||
lastThis === this && | ||
newArgs.length === lastArgs.length && | ||
lastArgs.every((lastArg, i) => isEqual(lastArg, newArgs[i]))) { | ||
@@ -20,2 +24,3 @@ return lastResult; | ||
calledOnce = true; | ||
lastThis = this; | ||
lastArgs = newArgs; | ||
@@ -22,0 +27,0 @@ lastResult = resultFn.apply(this, newArgs); |
@@ -7,24 +7,69 @@ // @flow | ||
type Expectation = {| | ||
args: any[], | ||
result: any | ||
|}; | ||
describe('memoizeOne', () => { | ||
function getA() { | ||
// $FlowSuppressError: allowing many values for `this` | ||
return this.a; | ||
} | ||
type Input = {| | ||
name: string, | ||
first: Expectation, | ||
second: Expectation | ||
|}; | ||
describe('standard behaviour - baseline', () => { | ||
let add; | ||
let memoizedAdd; | ||
describe('memoizeOne', () => { | ||
// [JavaScript defines seven built-in types:](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md) | ||
// - null | ||
// - undefined | ||
// - boolean | ||
// - number | ||
// - string | ||
// - object | ||
// - symbol | ||
beforeEach(() => { | ||
add = sinon.spy((value1: number, value2: number): number => value1 + value2); | ||
memoizedAdd = memoizeOne(add); | ||
}); | ||
describe('standard behaviour', () => { | ||
it('should return the result of a function', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
}); | ||
it('should return the same result if the arguments have not changed', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
}); | ||
it('should not execute the memoized function if the arguments have not changed', () => { | ||
memoizedAdd(1, 2); | ||
memoizedAdd(1, 2); | ||
expect(add.callCount).to.equal(1); | ||
}); | ||
it('should invalidate a memoize cache if new arguments are provided', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
}); | ||
it('should resume memoization after a cache invalidation', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(add.callCount).to.equal(1); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
}); | ||
}); | ||
describe('standard behaviour - dynamic', () => { | ||
type Expectation = {| | ||
args: any[], | ||
result: any | ||
|}; | ||
type Input = {| | ||
name: string, | ||
first: Expectation, | ||
second: Expectation | ||
|}; | ||
// [JavaScript defines seven built-in types:](https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch1.md) | ||
// - null | ||
// - undefined | ||
// - boolean | ||
// - number | ||
// - string | ||
// - object | ||
// - symbol | ||
const inputs: Input[] = [ | ||
@@ -132,3 +177,3 @@ { | ||
inputs.forEach(({ name, first, second }) => { | ||
describe(`dynamic type test:[${name}]`, () => { | ||
describe(`type: [${name}]`, () => { | ||
@@ -189,6 +234,16 @@ let spy; | ||
describe('original function', () => { | ||
function getA() { | ||
return this.a; | ||
} | ||
it('should respect new bindings', () => { | ||
const Foo = function (bar) { | ||
this.bar = bar; | ||
}; | ||
const memoized = memoizeOne(function (bar) { | ||
return new Foo(bar); | ||
}); | ||
const result = memoized('baz'); | ||
expect(result instanceof Foo).to.equal(true); | ||
expect(result.bar).to.equal('baz'); | ||
}); | ||
it('should respect explicit bindings', () => { | ||
@@ -216,5 +271,36 @@ const temp = { | ||
it('should respect default bindings', () => { | ||
const memoized = memoizeOne(getA); | ||
it('should respect implicit bindings', () => { | ||
const temp = { | ||
a: 2, | ||
getA, | ||
}; | ||
const memoized = memoizeOne(function () { | ||
return temp.getA(); | ||
}); | ||
expect(memoized()).to.equal(2); | ||
}); | ||
it('should respect fat arrow bindings', () => { | ||
const temp = { | ||
a: 50, | ||
}; | ||
function foo() { | ||
// return an arrow function | ||
return () => { | ||
// `this` here is lexically adopted from `foo()` | ||
return getA.call(this); | ||
}; | ||
} | ||
const bound = foo.call(temp); | ||
const memoized = memoizeOne(bound); | ||
expect(memoized()).to.equal(50); | ||
}); | ||
it('should respect ignored bindings', () => { | ||
const bound = getA.bind(null); | ||
const memoized = memoizeOne(bound); | ||
expect(memoized).to.throw(TypeError); | ||
@@ -225,6 +311,16 @@ }); | ||
describe('memoized function', () => { | ||
function getA() { | ||
return this.a; | ||
} | ||
it('should respect new bindings', () => { | ||
const memoizedGetA = memoizeOne(getA); | ||
const Foo = function (a) { | ||
this.a = a; | ||
this.result = memoizedGetA.call(this); | ||
}; | ||
const foo1 = new Foo(10); | ||
const foo2 = new Foo(20); | ||
expect(foo1.result).to.equal(10); | ||
expect(foo2.result).to.equal(20); | ||
}); | ||
it('should respect implicit bindings', () => { | ||
@@ -272,5 +368,60 @@ const getAMemoized = memoizeOne(getA); | ||
}); | ||
it('should respect implicit bindings', () => { | ||
const getAMemoized = memoizeOne(getA); | ||
const temp = { | ||
a: 2, | ||
getAMemoized, | ||
}; | ||
expect(temp.getAMemoized()).to.equal(2); | ||
}); | ||
it('should respect fat arrow bindings', () => { | ||
const temp = { | ||
a: 50, | ||
}; | ||
const memoizedGetA = memoizeOne(getA); | ||
function foo() { | ||
// return an arrow function | ||
return () => { | ||
// `this` here is lexically adopted from `foo()` | ||
return memoizedGetA.call(this); | ||
}; | ||
} | ||
const bound = foo.call(temp); | ||
const memoized = memoizeOne(bound); | ||
expect(memoized()).to.equal(50); | ||
}); | ||
it('should respect ignored bindings', () => { | ||
const memoized = memoizeOne(getA); | ||
const getResult = function () { | ||
return memoized.call(null); | ||
}; | ||
expect(getResult).to.throw(TypeError); | ||
}); | ||
}); | ||
}); | ||
describe('context change', () => { | ||
it('should break the memoization cache if the execution context changes', () => { | ||
const memoized = memoizeOne(getA); | ||
const temp1 = { | ||
a: 20, | ||
getMemoizedA: memoized, | ||
}; | ||
const temp2 = { | ||
a: 30, | ||
getMemoizedA: memoized, | ||
}; | ||
expect(temp1.getMemoizedA()).to.equal(20); | ||
expect(temp2.getMemoizedA()).to.equal(30); | ||
}); | ||
}); | ||
describe('custom equality function', () => { | ||
@@ -338,44 +489,3 @@ let add; | ||
}); | ||
describe('standard behaviour', () => { | ||
let add; | ||
let memoizedAdd; | ||
beforeEach(() => { | ||
add = sinon.spy((value1: number, value2: number): number => value1 + value2); | ||
memoizedAdd = memoizeOne(add); | ||
}); | ||
it('should return the result of a function', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
}); | ||
it('should return the same result if the arguments have not changed', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
}); | ||
it('should not execute the memoized function if the arguments have not changed', () => { | ||
memoizedAdd(1, 2); | ||
memoizedAdd(1, 2); | ||
expect(add.callCount).to.equal(1); | ||
}); | ||
it('should invalidate a memoize cache if new arguments are provided', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
}); | ||
it('should resume memoization after a cache invalidation', () => { | ||
expect(memoizedAdd(1, 2)).to.equal(3); | ||
expect(add.callCount).to.equal(1); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
expect(memoizedAdd(2, 2)).to.equal(4); | ||
expect(add.callCount).to.equal(2); | ||
}); | ||
}); | ||
}); | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
137544
457
0
132