You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import isObject from './isObject'
|
|
import root from './internal/root'
|
|
|
|
/**
|
|
* Creates a debounced function that delays invoking `func` until after `wait`
|
|
* milliseconds have elapsed since the last time the debounced function was
|
|
* invoked, or until the next browser frame is drawn. The debounced function
|
|
* comes with a `cancel` method to cancel delayed `func` invocations and a
|
|
* `flush` method to immediately invoke them. Provide `options` to indicate
|
|
* whether `func` should be invoked on the leading and/or trailing edge of the
|
|
* `wait` timeout. The `func` is invoked with the last arguments provided to the
|
|
* debounced function. Subsequent calls to the debounced function return the
|
|
* result of the last `func` invocation.
|
|
*
|
|
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
|
* invoked on the trailing edge of the timeout only if the debounced function
|
|
* is invoked more than once during the `wait` timeout.
|
|
*
|
|
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
|
* until the next tick, similar to `setTimeout` with a timeout of `0`.
|
|
*
|
|
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
|
|
* invocation will be deferred until the next frame is drawn (typically about
|
|
* 16ms).
|
|
*
|
|
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
|
* for details over the differences between `debounce` and `throttle`.
|
|
*
|
|
* @since 0.1.0
|
|
* @category Function
|
|
* @param {Function} func The function to debounce.
|
|
* @param {number} [wait=0]
|
|
* The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
|
|
* used (if available).
|
|
* @param {Object} [options={}] The options object.
|
|
* @param {boolean} [options.leading=false]
|
|
* Specify invoking on the leading edge of the timeout.
|
|
* @param {number} [options.maxWait]
|
|
* The maximum time `func` is allowed to be delayed before it's invoked.
|
|
* @param {boolean} [options.trailing=true]
|
|
* Specify invoking on the trailing edge of the timeout.
|
|
* @returns {Function} Returns the new debounced function.
|
|
* @example
|
|
*
|
|
* // Avoid costly calculations while the window size is in flux.
|
|
* jQuery(window).on('resize', debounce(calculateLayout, 150))
|
|
*
|
|
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
|
|
* jQuery(element).on('click', debounce(sendMail, 300, {
|
|
* 'leading': true,
|
|
* 'trailing': false
|
|
* }))
|
|
*
|
|
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
|
|
* const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
|
|
* const source = new EventSource('/stream')
|
|
* jQuery(source).on('message', debounced)
|
|
*
|
|
* // Cancel the trailing debounced invocation.
|
|
* jQuery(window).on('popstate', debounced.cancel)
|
|
*
|
|
* // Check for pending invocations.
|
|
* const status = debounced.pending() ? "Pending..." : "Ready"
|
|
*/
|
|
function debounce(func, wait, options?) {
|
|
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime
|
|
|
|
let lastInvokeTime = 0
|
|
let leading = false
|
|
let maxing = false
|
|
let trailing = true
|
|
|
|
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
|
|
const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === 'function'
|
|
|
|
if (typeof func !== 'function') {
|
|
throw new TypeError('Expected a function')
|
|
}
|
|
wait = +wait || 0
|
|
if (isObject(options)) {
|
|
leading = !!options.leading
|
|
maxing = 'maxWait' in options
|
|
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
|
|
trailing = 'trailing' in options ? !!options.trailing : trailing
|
|
}
|
|
|
|
function invokeFunc(time) {
|
|
const args = lastArgs
|
|
const thisArg = lastThis
|
|
|
|
lastArgs = lastThis = undefined
|
|
lastInvokeTime = time
|
|
result = func.apply(thisArg, args)
|
|
return result
|
|
}
|
|
|
|
function startTimer(pendingFunc, wait) {
|
|
if (useRAF) {
|
|
root.cancelAnimationFrame(timerId)
|
|
return root.requestAnimationFrame(pendingFunc)
|
|
}
|
|
return setTimeout(pendingFunc, wait)
|
|
}
|
|
|
|
function cancelTimer(id) {
|
|
if (useRAF) {
|
|
return root.cancelAnimationFrame(id)
|
|
}
|
|
clearTimeout(id)
|
|
}
|
|
|
|
function leadingEdge(time) {
|
|
// Reset any `maxWait` timer.
|
|
lastInvokeTime = time
|
|
// Start the timer for the trailing edge.
|
|
timerId = startTimer(timerExpired, wait)
|
|
// Invoke the leading edge.
|
|
return leading ? invokeFunc(time) : result
|
|
}
|
|
|
|
function remainingWait(time) {
|
|
const timeSinceLastCall = time - lastCallTime
|
|
const timeSinceLastInvoke = time - lastInvokeTime
|
|
const timeWaiting = wait - timeSinceLastCall
|
|
|
|
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting
|
|
}
|
|
|
|
function shouldInvoke(time) {
|
|
const timeSinceLastCall = time - lastCallTime
|
|
const timeSinceLastInvoke = time - lastInvokeTime
|
|
|
|
// Either this is the first call, activity has stopped and we're at the
|
|
// trailing edge, the system time has gone backwards and we're treating
|
|
// it as the trailing edge, or we've hit the `maxWait` limit.
|
|
return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait)
|
|
}
|
|
|
|
function timerExpired() {
|
|
const time = Date.now()
|
|
if (shouldInvoke(time)) {
|
|
return trailingEdge(time)
|
|
}
|
|
// Restart the timer.
|
|
timerId = startTimer(timerExpired, remainingWait(time))
|
|
}
|
|
|
|
function trailingEdge(time) {
|
|
timerId = undefined
|
|
|
|
// Only invoke if we have `lastArgs` which means `func` has been
|
|
// debounced at least once.
|
|
if (trailing && lastArgs) {
|
|
return invokeFunc(time)
|
|
}
|
|
lastArgs = lastThis = undefined
|
|
return result
|
|
}
|
|
|
|
function cancel() {
|
|
if (timerId !== undefined) {
|
|
cancelTimer(timerId)
|
|
}
|
|
lastInvokeTime = 0
|
|
lastArgs = lastCallTime = lastThis = timerId = undefined
|
|
}
|
|
|
|
function flush() {
|
|
return timerId === undefined ? result : trailingEdge(Date.now())
|
|
}
|
|
|
|
function pending() {
|
|
return timerId !== undefined
|
|
}
|
|
|
|
function debounced(this: any, ...args) {
|
|
const time = Date.now()
|
|
const isInvoking = shouldInvoke(time)
|
|
|
|
lastArgs = args
|
|
lastThis = this
|
|
lastCallTime = time
|
|
|
|
if (isInvoking) {
|
|
if (timerId === undefined) {
|
|
return leadingEdge(lastCallTime)
|
|
}
|
|
if (maxing) {
|
|
// Handle invocations in a tight loop.
|
|
timerId = startTimer(timerExpired, wait)
|
|
return invokeFunc(lastCallTime)
|
|
}
|
|
}
|
|
if (timerId === undefined) {
|
|
timerId = startTimer(timerExpired, wait)
|
|
}
|
|
return result
|
|
}
|
|
|
|
debounced.cancel = cancel
|
|
debounced.flush = flush
|
|
debounced.pending = pending
|
|
return debounced
|
|
}
|
|
|
|
export default debounce
|