Co-authored-by: Kristján Oddsson <koddsson@gmail.com>
This commit is contained in:
Keith Cirkel 2019-05-17 12:35:19 +01:00
Родитель fe4cdfafb0
Коммит 9372373105
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E0736F11348DDD3A
12 изменённых файлов: 6231 добавлений и 0 удалений

11
.flowconfig Normal file
Просмотреть файл

@ -0,0 +1,11 @@
[ignore]
[include]
[libs]
[lints]
[options]
[strict]

2
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
node_modules/
dist/

6
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,6 @@
language: node_js
node_js:
- "node"
cache:
directories:
- node_modules

55
README.md Normal file
Просмотреть файл

@ -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 |
```

16
index.d.ts поставляемый Normal file
Просмотреть файл

@ -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

51
index.js Normal file
Просмотреть файл

@ -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})
}

19
index.js.flow Normal file
Просмотреть файл

@ -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

5814
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

88
package.json Normal file
Просмотреть файл

@ -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"
}
}

2
prettier.config.js Normal file
Просмотреть файл

@ -0,0 +1,2 @@
/* @flow strict */
module.exports = require('eslint-plugin-github/prettier.config')

159
test/index.js Normal file
Просмотреть файл

@ -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])
})
})

8
test/index.ts Normal file
Просмотреть файл

@ -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})