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})
|
Загрузка…
Ссылка в новой задаче