feat: initial implementation
Co-authored-by: Kristján Oddsson <koddsson@gmail.com>
This commit is contained in:
Родитель
fe4cdfafb0
Коммит
9372373105
|
@ -0,0 +1,11 @@
|
|||
[ignore]
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
|
||||
[strict]
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
|
@ -0,0 +1,6 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
|
@ -0,0 +1,55 @@
|
|||
# mini-throttle
|
||||
|
||||
This is a package which provides `throttle` and `debounce` functions, with both
|
||||
flow and TypeScript declarations, and a minimal code footprint (less than 60
|
||||
lines, less than 350 bytes minified+gzipped)
|
||||
|
||||
|
||||
### throttling, debouncing, and everything inbetween
|
||||
|
||||
```js
|
||||
type ThrottleOptions = {
|
||||
start?: boolean, // fire immediately on the first call
|
||||
middle?: boolean, // if true, fire as soon as `wait` has passed
|
||||
once?: boolean, // cancel after the first successful call
|
||||
}
|
||||
function throttle<T>(
|
||||
callback: (...args: T[]) => any,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...args: T[]) => void
|
||||
|
||||
function debounce<T>(
|
||||
callback: (...args: T[]) => any,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...args: T[]) => void
|
||||
```
|
||||
|
||||
This package comes with two functions; `throttle` and `debounce`.
|
||||
|
||||
Both of these functions offer the exact same signature, because they're both
|
||||
the same function - just with different `opts` defaults:
|
||||
|
||||
- `throttle` opts default to `{ start: true, middle: true, once: false }`.
|
||||
- `debounce` opts default to `{ start: false, middle: false, once: false }`.
|
||||
|
||||
Each of the options changes when `callback` gets called. The best way to
|
||||
illustrate this is with a marble diagram.
|
||||
|
||||
```js
|
||||
for (let i = 1; i <= 10; ++i) {
|
||||
fn(i)
|
||||
await delay(50)
|
||||
}
|
||||
await delay(100)
|
||||
```
|
||||
```
|
||||
| fn() | 1 2 3 4 5 6 7 8 9 10 |
|
||||
| throttle(fn, 100}) | 1 2 4 6 8 10 |
|
||||
| throttle(fn, 100, {start: false}) | 2 4 6 8 10 |
|
||||
| throttle(fn, 100, {middle: false}) | 1 10 |
|
||||
| throttle(fn, 100, {once: true}) | 1 |
|
||||
| throttle(fn, 100, {once: true, start: false})| 2 |
|
||||
| debounce(fn, 100) | 10 |
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
export type ThrottleOptions = {
|
||||
start?: boolean,
|
||||
middle?: boolean,
|
||||
once?: boolean
|
||||
}
|
||||
export function throttle<T>(
|
||||
callback: (...args: T[]) => any,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...args: T[]) => void
|
||||
|
||||
export function debounce<T>(
|
||||
callback: (...args: T[]) => any,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...args: T[]) => void
|
|
@ -0,0 +1,51 @@
|
|||
/* @flow strict */
|
||||
|
||||
type ThrottleOptions = {|
|
||||
start?: boolean,
|
||||
middle?: boolean,
|
||||
once?: boolean
|
||||
|}
|
||||
export function throttle<T: $ReadOnlyArray<mixed>>(
|
||||
callback: (...T) => mixed,
|
||||
wait: number = 0,
|
||||
{start = true, middle = true, once = false}: ThrottleOptions = {}
|
||||
): (...T) => void {
|
||||
let last = 0
|
||||
let timer
|
||||
let cancelled = false
|
||||
const fn = (...args) => {
|
||||
if (cancelled) return
|
||||
const delta = Date.now() - last
|
||||
last = Date.now()
|
||||
if (start) {
|
||||
//eslint-disable-next-line flowtype/no-flow-fix-me-comments
|
||||
// $FlowFixMe this isn't a const
|
||||
start = false
|
||||
callback(...args)
|
||||
if (once) fn.cancel()
|
||||
} else if ((middle && delta < wait) || !middle) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(
|
||||
() => {
|
||||
last = Date.now()
|
||||
callback(...args)
|
||||
if (once) fn.cancel()
|
||||
},
|
||||
!middle ? wait : wait - delta
|
||||
)
|
||||
}
|
||||
}
|
||||
fn.cancel = () => {
|
||||
clearTimeout(timer)
|
||||
cancelled = true
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
export function debounce<T: $ReadOnlyArray<mixed>>(
|
||||
callback: (...T) => mixed,
|
||||
wait: number = 0,
|
||||
{start = false, middle = false, once = false}: ThrottleOptions = {}
|
||||
): (...T) => void {
|
||||
return throttle(callback, wait, {start, middle, once})
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/* @flow strict */
|
||||
|
||||
type ThrottleOptions = {|
|
||||
start?: boolean,
|
||||
middle?: boolean,
|
||||
once?: boolean
|
||||
|}
|
||||
|
||||
declare export function throttle<T: $ReadOnlyArray<mixed>>(
|
||||
callback: (...T) => mixed,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...T) => void
|
||||
|
||||
declare export function debounce<T: $ReadOnlyArray<mixed>>(
|
||||
callback: (...T) => mixed,
|
||||
wait: number,
|
||||
opts?: ThrottleOptions
|
||||
): (...T) => void
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"name": "@github/mini-throttle",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"repository": "github.com/github/mini-throttle",
|
||||
"license": "MIT",
|
||||
"author": "GitHub Inc. (https://github.com)",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"prebuild": "npm run clean && npm run lint && mkdir dist",
|
||||
"build": "npm run build-umd && npm run build-esm && cp index.d.ts dist/index.d.ts",
|
||||
"build-esm": "BABEL_ENV=esm babel index.js -o dist/index.esm.js && cp index.js.flow dist/index.esm.js.flow",
|
||||
"build-umd": "BABEL_ENV=umd babel index.js -o dist/index.umd.js && cp index.js.flow dist/index.umd.js.flow",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "github-lint",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "BABEL_ENV=umd mocha --require @babel/register && npm run tsc",
|
||||
"tsc": "tsc --noEmit --strict test/index.ts"
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"esm": {
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"modules": false,
|
||||
"exclude": [
|
||||
"@babel/plugin-transform-regenerator",
|
||||
"@babel/plugin-transform-spread",
|
||||
"@babel/plugin-transform-destructuring",
|
||||
"@babel/plugin-transform-parameters"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@babel/flow"
|
||||
]
|
||||
},
|
||||
"umd": {
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-modules-umd"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"exclude": [
|
||||
"@babel/plugin-transform-regenerator",
|
||||
"@babel/plugin-transform-spread",
|
||||
"@babel/plugin-transform-destructuring",
|
||||
"@babel/plugin-transform-parameters"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@babel/flow"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"plugin:github/browser",
|
||||
"plugin:github/es6",
|
||||
"plugin:github/flow",
|
||||
"plugin:escompat/recommended"
|
||||
]
|
||||
},
|
||||
"eslintIgnore": ["node_modules/", "dist/"],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/register": "^7.4.4",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-escompat": "^1.0.3",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"flow-bin": "^0.98.1",
|
||||
"mocha": "^6.1.4",
|
||||
"typescript": "^3.4.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/* @flow strict */
|
||||
module.exports = require('eslint-plugin-github/prettier.config')
|
|
@ -0,0 +1,159 @@
|
|||
/* @flow */
|
||||
import {throttle, debounce} from '../index'
|
||||
import {beforeEach, describe, it} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
const delay = m => new Promise(r => setTimeout(r, m))
|
||||
|
||||
let calls
|
||||
let fn
|
||||
beforeEach(() => {
|
||||
fn && fn.cancel && fn.cancel()
|
||||
calls = []
|
||||
})
|
||||
describe('throttle', () => {
|
||||
beforeEach(() => {
|
||||
fn = throttle((...xs) => calls.push(xs), 100)
|
||||
})
|
||||
it('fires callback immediately', async () => {
|
||||
fn()
|
||||
expect(calls).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('calls callback with given arguments', async () => {
|
||||
fn(1, 2, 3)
|
||||
expect(calls).to.eql([[1, 2, 3]])
|
||||
})
|
||||
|
||||
it('fires once `wait` ms have passed', async () => {
|
||||
fn(1)
|
||||
await delay(50)
|
||||
fn(2)
|
||||
await delay(50)
|
||||
fn(3)
|
||||
await delay(50)
|
||||
fn(4)
|
||||
expect(calls).to.eql([[1], [2]])
|
||||
})
|
||||
|
||||
it('will fire last call after `wait` ms have passed', async () => {
|
||||
fn(1)
|
||||
fn(2)
|
||||
fn(3)
|
||||
await delay(100)
|
||||
expect(calls).to.eql([[1], [3]])
|
||||
})
|
||||
|
||||
it('calls callback with given arguments (middle)', async () => {
|
||||
fn(1, 2, 3)
|
||||
fn(4, 5, 6)
|
||||
fn(7, 8, 9)
|
||||
fn(10, 11, 12)
|
||||
await delay(100)
|
||||
expect(calls).to.eql([[1, 2, 3], [10, 11, 12]])
|
||||
})
|
||||
|
||||
it('can be cancelled with cancel()', async () => {
|
||||
fn.cancel()
|
||||
fn(1, 2, 3)
|
||||
fn(4, 5, 6)
|
||||
fn(7, 8, 9)
|
||||
fn(10, 11, 12)
|
||||
await delay(100)
|
||||
expect(calls).to.eql([])
|
||||
})
|
||||
|
||||
it('does not expose `this`', async () => {
|
||||
fn = throttle(function() {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
calls.push(this)
|
||||
}, 100)
|
||||
fn(1)
|
||||
expect(calls).to.eql([undefined])
|
||||
})
|
||||
})
|
||||
|
||||
describe('throttle {start:false}', () => {
|
||||
beforeEach(() => {
|
||||
fn = throttle((...xs) => calls.push(xs), 100, {start: false})
|
||||
})
|
||||
it('does not fire callback immediately', async () => {
|
||||
fn()
|
||||
expect(calls).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('throttle {middle:false}', () => {
|
||||
beforeEach(() => {
|
||||
fn = throttle((...xs) => calls.push(xs), 100, {middle: false})
|
||||
})
|
||||
|
||||
it('fires first callback', async () => {
|
||||
fn(1)
|
||||
expect(calls).to.eql([[1]])
|
||||
})
|
||||
|
||||
it('does not fire if `wait` ms have passed', async () => {
|
||||
fn(1)
|
||||
await delay(50)
|
||||
fn(2)
|
||||
await delay(50)
|
||||
fn(3)
|
||||
await delay(50)
|
||||
fn(4)
|
||||
expect(calls).to.eql([[1]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('debounce (throttle with {start: false, middle: false})', () => {
|
||||
beforeEach(() => {
|
||||
fn = debounce((...xs) => calls.push(xs), 100)
|
||||
})
|
||||
|
||||
it('does not fire callback immediately', async () => {
|
||||
fn()
|
||||
expect(calls).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('only fires once `wait` ms have passed without any calls', async () => {
|
||||
fn(1)
|
||||
fn(2)
|
||||
fn(3)
|
||||
await delay(100)
|
||||
expect(calls).to.eql([[3]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('marbles', () => {
|
||||
const loop = async cb => {
|
||||
for (let i = 1; i <= 10; ++i) {
|
||||
cb(i)
|
||||
await delay(50)
|
||||
}
|
||||
await delay(100)
|
||||
}
|
||||
|
||||
it('fn', async () => {
|
||||
await loop(x => calls.push(x))
|
||||
expect(calls).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
})
|
||||
|
||||
it('throttle(fn, 100)', async () => {
|
||||
await loop(throttle(x => calls.push(x), 100))
|
||||
expect(calls).to.eql([1, 2, 4, 6, 8, 10])
|
||||
})
|
||||
|
||||
it('throttle(fn, 100, {start:false})', async () => {
|
||||
await loop(throttle(x => calls.push(x), 100, {start: false}))
|
||||
expect(calls).to.eql([2, 4, 6, 8, 10])
|
||||
})
|
||||
|
||||
it('throttle(fn, 100, {middle:false})', async () => {
|
||||
await loop(throttle(x => calls.push(x), 100, {middle: false}))
|
||||
expect(calls).to.eql([1, 10])
|
||||
})
|
||||
|
||||
it('debounce(fn, 100)', async () => {
|
||||
await loop(debounce(x => calls.push(x), 100))
|
||||
expect(calls).to.eql([10])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
import {throttle, debounce} from '../index'
|
||||
|
||||
throttle(() => {}, 100)
|
||||
throttle(() => {}, 100, {start: false})
|
||||
throttle(() => {}, 100, {middle: false})
|
||||
debounce(() => {}, 100)
|
||||
debounce(() => {}, 100, {start: false})
|
||||
debounce(() => {}, 100, {middle: false})
|
Загрузка…
Ссылка в новой задаче