From f1845856846a8691b5a0c27fdcc660449160b0c1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 22 Nov 2017 08:32:37 +1100 Subject: [PATCH 1/3] adding cancel function --- src/index.js | 7 +++++++ test/index.spec.js | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/index.js b/src/index.js index cbfc31e..cf5913f 100644 --- a/src/index.js +++ b/src/index.js @@ -23,5 +23,12 @@ export default (fn: Function): ResultFn => { return frameId; }; + // Adding cancel property to result function + result.cancel = () => { + if (frameId) { + cancelAnimationFrame(frameId); + } + }; + return result; }; \ No newline at end of file diff --git a/test/index.spec.js b/test/index.spec.js index 14a8436..6b1cacf 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -105,6 +105,17 @@ describe('behaviour', () => { expect(myMock).toHaveBeenCalledTimes(0); }); + + it('should allow cancelling of the frame directly', () => { + const myMock = jest.fn(); + const fn = rafScheduler(myMock); + + fn.cancel(); + // would normally release the function + requestAnimationFrame.step(); + + expect(myMock).toHaveBeenCalledTimes(0); + }); }); describe('respecting original "this" context', () => { From d1c31a49d71801229fed17cf0ad2d8415c244ad1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 22 Nov 2017 09:19:49 +1100 Subject: [PATCH 2/3] adding .cancel property to ResultFn --- README.md | 35 ++++++++++++++++++++----- src/index.js | 21 ++++++++++----- test/index.spec.js | 64 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 92 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 43a6812..e24af0c 100644 --- a/README.md +++ b/README.md @@ -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'; @@ -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); @@ -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. diff --git a/src/index.js b/src/index.js index cf5913f..121101d 100644 --- a/src/index.js +++ b/src/index.js @@ -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; @@ -24,11 +28,16 @@ export default (fn: Function): ResultFn => { }; // Adding cancel property to result function - result.cancel = () => { - if (frameId) { - cancelAnimationFrame(frameId); + wrapperFn.cancel = () => { + if (!frameId) { + return; } + + cancelAnimationFrame(frameId); + frameId = null; }; - return result; + const resultFn: ResultFn = (wrapperFn: any); + + return resultFn; }; \ No newline at end of file diff --git a/test/index.spec.js b/test/index.spec.js index 6b1cacf..40e54e1 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,6 +1,6 @@ // @flow import { replaceRaf } from 'raf-stub'; -import rafScheduler from '../src/'; +import rafSchedule from '../src/'; replaceRaf(); @@ -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(); @@ -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(); @@ -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(); @@ -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); @@ -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); @@ -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); @@ -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); @@ -106,16 +106,35 @@ describe('behaviour', () => { expect(myMock).toHaveBeenCalledTimes(0); }); - it('should allow cancelling of the frame directly', () => { + it('should allow cancelling of a frame using .cancel', () => { const myMock = jest.fn(); - const fn = rafScheduler(myMock); + 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', () => { @@ -128,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(); }); @@ -147,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(); @@ -164,7 +183,7 @@ describe('respecting original "this" context', () => { }, }; - const schedule = rafScheduler(function () { + const schedule = rafSchedule(function () { foo.callMock(); }); @@ -180,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); }); @@ -190,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(); + }); +}); + From 999053c5421210e6b5a068835290503e98c029fc Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 22 Nov 2017 09:21:06 +1100 Subject: [PATCH 3/3] bumping dev dependencies --- package.json | 8 ++++---- yarn.lock | 56 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 20ebb04..00d2879 100644 --- a/package.json +++ b/package.json @@ -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": {}, diff --git a/yarn.lock b/yarn.lock index e0de854..4c882c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -83,7 +83,7 @@ acorn@^4.0.4: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.1.1: +acorn@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" @@ -98,14 +98,14 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" +ajv@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" - json-stable-stringify "^1.0.1" align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" @@ -1208,9 +1208,9 @@ escodegen@^1.6.1: optionalDependencies: source-map "~0.2.0" -eslint-plugin-jest@21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.2.0.tgz#292044df9cf0866ad9c530e78e6528fae287b926" +eslint-plugin-jest@21.3.2: + version "21.3.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.3.2.tgz#b1cefc05f0fed700eb40185a94d16f6d575d1ef9" eslint-scope@^3.7.1: version "3.7.1" @@ -1219,11 +1219,11 @@ eslint-scope@^3.7.1: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.10.0.tgz#f25d0d7955c81968c2309aa5c9a229e045176bb7" +eslint@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.11.0.tgz#39a8c82bc0a3783adf5a39fa27fdd9d36fac9a34" dependencies: - ajv "^5.2.0" + ajv "^5.3.0" babel-code-frame "^6.22.0" chalk "^2.1.0" concat-stream "^1.6.0" @@ -1231,7 +1231,7 @@ eslint@4.10.0: debug "^3.0.1" doctrine "^2.0.0" eslint-scope "^3.7.1" - espree "^3.5.1" + espree "^3.5.2" esquery "^1.0.0" estraverse "^4.2.0" esutils "^2.0.2" @@ -1244,7 +1244,7 @@ eslint@4.10.0: inquirer "^3.0.6" is-resolvable "^1.0.0" js-yaml "^3.9.1" - json-stable-stringify "^1.0.1" + json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" lodash "^4.17.4" minimatch "^3.0.2" @@ -1261,11 +1261,11 @@ eslint@4.10.0: table "^4.0.1" text-table "~0.2.0" -espree@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e" +espree@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" dependencies: - acorn "^5.1.1" + acorn "^5.2.1" acorn-jsx "^3.0.0" esprima@^2.7.1: @@ -1368,6 +1368,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -1434,9 +1438,9 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" -flow-bin@0.58.0: - version "0.58.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.58.0.tgz#62d5a776589419e5656800a0e5230a5e585ca65e" +flow-bin@0.59.0: + version "0.59.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.59.0.tgz#8c151ee7f09f1deed9bf0b9d1f2e8ab9d470f1bb" for-in@^1.0.1: version "1.0.2" @@ -2207,6 +2211,10 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" @@ -2661,9 +2669,9 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.1.tgz#91064d778c08c85ac1cbe6b23195c34310d039f9" +prettier@1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" pretty-format@^21.2.1: version "21.2.1"