Why do you want to write this article?

You’ve read a lot of articles about throttling/stabilization, most of which are split into immediate and non-immediate versions. In recent use of the process, I found that both versions of the input box when the input results are not ideal. Let’s take a look at the corresponding scenario and how to optimize it.

Ii. Usage Scenarios

1. Not immediately executed

The non-immediate version is that the event is executed only once, at the end, as long as the input interval does not exceed the set interval.

  • The corresponding code is:
/** * Not immediately executable version *@author waldon
 * @date The 2021-05-01 *@returns {Function} - Anti-shaking function */
const debounce = (function () {
  let timer = 0
  return function (fn, delay = 300) {
    if (timer) {
      clearTimeout(timer)
    }
    // Not immediately executed
    timer = setTimeout(() = > {
      fn()
    }, delay)
  }
})()
Copy the code
  • Effect display:

  • defects

As you can see from the GIF above, it takes 300 milliseconds for the event to execute after the input value is changed. However, the v-Model of Vue uses compositionStart and compositionend to help us optimize the Chinese input. The general user will think of seeing the corresponding search results when typing the first word. This stabilization sacrifices the user experience in terms of speed.

2. Execute immediately

The immediate execution version is the event that only triggers the first keyword change during continuous input. It will not be triggered again unless the interval entered later is greater than the set interval

  • The corresponding code is:
/** * Instant execution version *@author waldon
 * @date The 2021-05-01 *@returns {Function} - Anti-shaking function */
const debounce = (function () {
  let timer = 0
  return function (fn, delay = 300, immediate = true) {
    if (timer) {
      clearTimeout(timer)
    }
    if (immediate) {
      constcallNow = ! timer timer =setTimeout(() = > {
        timer = 0
      }, delay)
      if (callNow) {
        fn()
      }
    } else {
      // Not immediately executed
      timer = setTimeout(() = > {
        fn()
      }, delay)
    }
  }
})()
Copy the code
  • Effect display:

  • defects

This instant stabilization is buggy when used in input boxes.

  1. When the input is fast, only the first keyword will be searched and the rest will be ignored. If used in the search list, this search result is definitely wrong
  2. Long press back/ Delete key delete, frequency is also very fast. The search results that delete the first keyword are still displayed even after the deletion.

At this time, a careful friend will think of. Why not trigger once at the beginning and end of the input? Let’s move on to the third one.

3. Execute immediately + Delay

The effect of this is a combination of the first two. The logic will be executed once when the first key word is entered, and then the logic will not be executed if continuous input is entered in the middle. When the input stops, the logic of the last input is executed again.

  • The corresponding code is:
/** * Repeat the execution version *@author waldon
 * @date The 2021-05-01 *@returns {Function} - Anti-shaking function */
const debounce = (function () {
  let timer = 0
  return function (fn, delay = 300, immediate = true) {
    if (timer) {
      clearTimeout(timer)
    }
    if (immediate) {
      constcallNow = ! timer timer =setTimeout(() = > {
        fn() // This step is more than the immediate execution version
        timer = 0
      }, delay)
      if (callNow) {
        fn()
      }
    } else {
      // Not immediately executed
      timer = setTimeout(() = > {
        fn()
      }, delay)
    }
  }
})()
Copy the code
  • Effect display:

  • review

Delays and inaccurate results have been addressed in this release. But more attentive friends might have noticed: “In the GIF above, only one keyword was entered, and it was executed twice.” If there is no logic in the project to handle repeated requests, wouldn’t there be two repeated requests? That definitely needs to be optimized.

4. Execute now + Delay + cacheKey

This has the same effect as the third version, except that a cacheKey field is added to determine whether the value entered last time is the same as the value entered last time, avoiding repeated logic.

  • The corresponding code is:
/** * cacheKey version *@author waldon
 * @date The 2021-05-01 *@returns {Function} - Anti-shaking function */
const debounce = (function () {
  let timer = 0
  let cacheKey = ' '
  return function (fn, delay = 300, immediate = true, key = ' ') {
    if (timer) {
      clearTimeout(timer)
    }
    if (immediate) {
      // Execute immediately
      letcallNow = ! timer timer =setTimeout(() = > {
        timer = 0
        if(cacheKey ! == key) { fn() } }, delay)if (callNow) {
        cacheKey = key
        fn()
      }
    } else {
      // Not immediately executed
      timer = setTimeout(() = > {
        fn()
      }, delay)
    }
  }
})()
Copy the code
  • Effect display:

  • review

Solved the problem of repeated execution of the same keyword. We can use the input value as the key in the input event, and the scrollTop value as the key in the pageScroll event. In fact, this can handle most scenarios, but some of the more special scenarios, when he triggered, no change value, then this definitely does not apply.

5. Execute now + Delay + lastTimer

This version is actually based on the source code of Lodash. The general idea is to assign the task pool ID to another variable the first time a scheduled task is defined. The timer is going to keep changing over time, but the lastTimer that was assigned at the beginning is not going to change. If the two values are not consistent, the callback function is not fired. After the continuous firing stops, the variables are reset in the callback function.

  • The corresponding code is:
/** * stabler function lastTimer version *@author waldon
 * @date The 2021-05-01 *@returns {Function} - Anti-shaking function */
const debounce = (function () {
  let timer = 0
  let lastTimer = 0
  return function (fn, delay = 300, immediate = true) {
    if (timer) {
      clearTimeout(timer)
    }
    if (immediate) {
      // Execute immediately
      letcallNow = ! timer timer =setTimeout(() = > {
        if(lastTimer ! == timer) { timer =0
          lastTimer = 0
          fn()
        }
      }, delay)
      if (callNow) {
        lastTimer = timer
        fn()
      }
    } else {
      // Not immediately executed
      timer = setTimeout(() = > {
        fn()
        timer = 0
      }, delay)
    }
  }
})()
Copy the code
  • Effect display:

The effect here is the same as in version 4, no duplicate textures.

  • Lodash debounce implementation source code
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`.
  constuseRAF = (! wait && wait ! = =0 && typeof root.requestAnimationFrame === 'function')

  if (typeoffunc ! = ='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' inoptions ? !!!!! 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() {
    returntimerId ! = =undefined
  }

  function debounced(. 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
}
Copy the code

Third, summary

The usage scenario here is for a very large number of users or requests that are very performance intensive. If server stress permits, the user experience will be better if throttling is used to give feedback at appropriate intervals.

Finally, May Day happy ~

Iv. Reference Resources

  • Github.com/lodash/loda…