Skip to content

Commit

Permalink
Merge pull request #7 from alexreardon/adding-cancel-utility
Browse files Browse the repository at this point in the history
Adding cancel utility
  • Loading branch information
alexreardon authored Nov 21, 2017
2 parents b2fb556 + 999053c commit 473cc47
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 51 deletions.
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,20 @@ window.addEventListener('scroll', function() {
```js
type rafSchedule = (fn: Function) => ResultFn

type ResultFn = (...arg: any[]) => number;
// Adding a .cancel property to the WrapperFn

type WrapperFn = (...arg: any[]) => number;
type CancelFn = {|
cancel: () => void,
|};
type ResultFn = WrapperFn & CancelFn;
```

At the top level `raf-schd` accepts any function a returns a new `ResultFn` (a function that wraps your original function). When executed, the `ResultFn` returns a `number`. This number is the animation frame id. You can use this frame id to cancel the scheduled frame using `cancelAnimationFrame(id)`;
At the top level `raf-schd` accepts any function a returns a new `ResultFn` (a function that wraps your original function). When executed, the `ResultFn` returns a `number`. This number is the animation frame id. You can cancel a frame using the `.cancel()` property on the `ResultFn`.

The `ResultFn` will execute your function with the **latest arguments** provided to it on the next animation frame.

**Throttled with latest argument**
### Throttled with latest argument

```js
import rafSchedule from 'raf-schd';
Expand All @@ -101,8 +107,25 @@ schedule(5, 6);
// do something called with => '5, 6'
```

### Cancelling a frame

#### `.cancel`

`raf-schd` adds a `.cancel` property to the `ResultFn` so that it can be easily cancelled. The frame will only be cancelled if it has not yet executed.

```js
const scheduled = rafSchedule(doSomething);

schedule('foo');

scheduled.cancel();

// now doSomething will not be executed in the next animation frame
```

#### `cancelAnimationFrame`

**Cancelling a frame**
You can use [`cancelAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame) directly to cancel a frame if you like. You can do this because you have the `frameId`.

```js
const scheduled = rafSchedule(doSomething);
Expand All @@ -120,14 +143,14 @@ cancelAnimationFrame(frameId);

Lets take a look at the characteristics of this library:

**Similiarities to `throttle`**
### Similarities to `throttle`

- It batches multiple calls into a single event
- It only executes the wrapped function with the latest argument
- It will not execute anything if the function is not invoked
- One invokation of a scheduled function always results in at least one function call, unless canceled. This is `throttle` with tail calls enabled.

**Differences to `throttle`**
### Differences to `throttle`

- Rather than throttling based on time (such as `200ms`, this library throttles based on `requestAnimationFrame`. This allows the browser to control how many frames to provide per second to optimise rendering.
- Individual frames of `raf-schd` can be canceled using `cancelAnimationFrame` as it returns the frame id.
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
"babel-jest": "^21.2.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-flow": "6.23.0",
"eslint": "4.10.0",
"eslint-plugin-jest": "21.2.0",
"flow-bin": "0.58.0",
"eslint": "4.11.0",
"eslint-plugin-jest": "21.3.2",
"flow-bin": "0.59.0",
"jest": "21.2.1",
"prettier": "1.8.1",
"prettier": "1.8.2",
"raf-stub": "2.0.1"
},
"dependencies": {},
Expand Down
22 changes: 19 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// @flow
type ResultFn = (...arg: any[]) => number;
type WrapperFn = (...arg: any[]) => number;
type CancelFn = {|
cancel: () => void,
|};
type ResultFn = WrapperFn & CancelFn;

export default (fn: Function): ResultFn => {
let lastArgs: any[] = [];
let frameId: ?number = null;

const result: ResultFn = (...args: any): number => {
const wrapperFn: WrapperFn = (...args: any): number => {
// Always capture the latest value
lastArgs = args;

Expand All @@ -23,5 +27,17 @@ export default (fn: Function): ResultFn => {
return frameId;
};

return result;
// Adding cancel property to result function
wrapperFn.cancel = () => {
if (!frameId) {
return;
}

cancelAnimationFrame(frameId);
frameId = null;
};

const resultFn: ResultFn = (wrapperFn: any);

return resultFn;
};
71 changes: 57 additions & 14 deletions test/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import { replaceRaf } from 'raf-stub';
import rafScheduler from '../src/';
import rafSchedule from '../src/';

replaceRaf();

Expand All @@ -11,7 +11,7 @@ beforeEach(() => {
describe('behaviour', () => {
it('should not execute a callback before a animation frame', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

fn();

Expand All @@ -20,7 +20,7 @@ describe('behaviour', () => {

it('should execute a callback after an animation frame', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

fn();
requestAnimationFrame.step();
Expand All @@ -30,7 +30,7 @@ describe('behaviour', () => {

it('should not execute multiple times if waiting for a frame', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

fn();
fn();
Expand All @@ -48,7 +48,7 @@ describe('behaviour', () => {

it('should execute the callback with the latest value', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

fn(1);
fn(2);
Expand All @@ -62,7 +62,7 @@ describe('behaviour', () => {

it('should execute the callbacks with the latest value when there are multiple args', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

fn(1, 2, 3);
fn(4, 5, 6);
Expand All @@ -75,7 +75,7 @@ describe('behaviour', () => {

it('should return the exact value that was passed to the callback', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);
const value = { hello: 'world' };

fn(value);
Expand All @@ -87,16 +87,16 @@ describe('behaviour', () => {

it('should return the frame id', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

const frameId: number = fn();

expect(frameId).toEqual(expect.any(Number));
});

it('should not execute the queued frame if it is cancelled', () => {
it('should allow cancelled of a frame using cancelAnimationFrame', () => {
const myMock = jest.fn();
const fn = rafScheduler(myMock);
const fn = rafSchedule(myMock);

const frameId: number = fn();
cancelAnimationFrame(frameId);
Expand All @@ -105,6 +105,36 @@ describe('behaviour', () => {

expect(myMock).toHaveBeenCalledTimes(0);
});

it('should allow cancelling of a frame using .cancel', () => {
const myMock = jest.fn();
const fn = rafSchedule(myMock);

fn(10);
fn.cancel();
// would normally release the function
requestAnimationFrame.step();

expect(myMock).toHaveBeenCalledTimes(0);
});

it('should permit future frames after cancelling a frame', () => {
const myMock = jest.fn();
const fn = rafSchedule(myMock);

// first frame is cancelled
fn(10);
fn.cancel();
// would normally release the function
requestAnimationFrame.step();

expect(myMock).toHaveBeenCalledTimes(0);

// second frame is not cancelled
fn(20);
requestAnimationFrame.step();
expect(myMock).toHaveBeenCalledWith(20);
});
});

describe('respecting original "this" context', () => {
Expand All @@ -117,7 +147,7 @@ describe('respecting original "this" context', () => {
return mock(this.a);
};
const foo = new Foo(10);
const schedule = rafScheduler(function () {
const schedule = rafSchedule(function () {
foo.callMock();
});

Expand All @@ -136,7 +166,7 @@ describe('respecting original "this" context', () => {
a: 50,
};
const bound = callMock.bind(foo);
const schedule = rafScheduler(bound);
const schedule = rafSchedule(bound);

schedule();
requestAnimationFrame.step();
Expand All @@ -153,7 +183,7 @@ describe('respecting original "this" context', () => {
},
};

const schedule = rafScheduler(function () {
const schedule = rafSchedule(function () {
foo.callMock();
});

Expand All @@ -169,7 +199,7 @@ describe('respecting original "this" context', () => {
// $ExpectError - this should throw!
mock(this.a);
};
const schedule = rafScheduler(function () {
const schedule = rafSchedule(function () {
callMock.call(null);
});

Expand All @@ -179,3 +209,16 @@ describe('respecting original "this" context', () => {
});
});

describe('flow type', () => {
it('should type the result function correctly', () => {
type FakeFn = (x: number) => number
const fakeFn: FakeFn = (x: number) => x;

const schedule: FakeFn = rafSchedule(fakeFn);

schedule(10);

schedule.cancel();
});
});

Loading

0 comments on commit 473cc47

Please sign in to comment.