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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
1
132