UNPKG

foop

Version:

interfaces that describe their intentions.

242 lines (214 loc) 7.17 kB
const isUndefined = require('../is/undefined') const isFunction = require('../is/function') const isObj = require('../is/obj') const noop = require('../util/noop') const toInteger = require('../cast/toInteger') // underscore.js // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. 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`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 5.0.0-beta.5 * * @param {Function} func The function to debounce. * @param {number} [wait=0] The number of milliseconds to delay. * @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. * @return {Function} Returns the new debounced function. * * @fork 0.1.0 * @category Function * @func * * {@link https://github.com/lodash/lodash/blob/master/debounce.js lodash-debounce} * @see {@link lodash-debounce} * * @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) * */ function debounce(func, wait, options) { let lastArgs let lastThis let maxWait let result let timerId let lastCallTime let lastInvokeTime = 0 let leading = false let maxing = false let trailing = true // @TODO this would be coercing if (!isFunction(func)) { func = noop // throw new TypeError('Expected a function') } // defaultTo(0) wait = toInteger(wait) // const optionsHas = hasIn(options) if (isObj(options)) { leading = !!options.leading maxing = 'maxWait' in options maxWait = maxing ? Math.max(toInteger(options.maxWait), 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 leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time // Start the timer for the trailing edge. timerId = setTimeout(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 (isUndefined(lastCallTime) || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)) } function timerExpired() { const time = Date.now() if (shouldInvoke(time)) { return trailingEdge(time) } // Restart the timer. timerId = setTimeout(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 (!isUndefined(timerId)) { clearTimeout(timerId) } lastInvokeTime = 0 lastArgs = lastCallTime = lastThis = timerId = undefined } function flush() { return isUndefined(timerId) ? result : trailingEdge(Date.now()) } function debounced(...args) { const time = Date.now() const isInvoking = shouldInvoke(time) lastArgs = args lastThis = this lastCallTime = time if (isInvoking) { if (isUndefined(timerId)) { return leadingEdge(lastCallTime) } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait) return invokeFunc(lastCallTime) } } if (isUndefined(timerId)) { timerId = setTimeout(timerExpired, wait) } return result } debounced.cancel = cancel debounced.flush = flush return debounced } module.exports = debounce // underscore js version // module.exports = function(func, wait, immediate) { // let timeout // let result // // const later = function(context, args) { // timeout = null // if (args) result = func.apply(context, args) // } // // const debounced = restArgs(function(args) { // if (timeout) clearTimeout(timeout) // if (immediate) { // const callNow = !timeout // timeout = setTimeout(later, wait) // if (callNow) result = func.apply(this, args) // } // else { // timeout = delay(later, wait, this, args) // } // // return result // }) // // debounced.cancel = function() { // clearTimeout(timeout) // timeout = null // } // // return debounced // }