function/throttle.jsjavascript
/**
 * 创建节流函数,支持前缘、尾调用以及 cancel/flush 控制。
 * @function throttle
 * @memberof module:function
 * @param {Function} fn 目标函数
 * @param {number} [wait=0] 节流窗口毫秒数
 * @param {Object} [options] 执行选项
 * @param {boolean} [options.leading=true] 是否在节流窗口开始时立即执行
 * @param {boolean} [options.trailing=true] 是否在节流窗口结束后执行
 * @returns {Function} 带 cancel 和 flush 方法的节流函数
 * @throws {TypeError|RangeError} 参数类型或 wait 不合法时抛出
 */
function throttle(fn, wait = 0, options = {}) {
  if (typeof fn !== 'function') throw new TypeError('fn must be a function')
  if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait must be a non-negative finite number')
  if (options === null || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('options must be an object')
  const leading = options.leading ?? true
  const trailing = options.trailing ?? true
  if (typeof leading !== 'boolean' || typeof trailing !== 'boolean') throw new TypeError('leading and trailing must be booleans')
  let timerId = null
  let lastInvoke = 0
  let lastArgs
  let lastThis
  let result
  const invoke = () => {
    const args = lastArgs
    const context = lastThis
    lastArgs = undefined
    lastThis = undefined
    lastInvoke = Date.now()
    result = fn.apply(context, args)
    return result
  }
  const trailingCall = () => {
    timerId = null
    if (trailing && lastArgs) invoke()
  }
  const throttled = function (...args) {
    const now = Date.now()
    if (lastInvoke === 0 && !leading) lastInvoke = now
    const remaining = wait - (now - lastInvoke)
    lastArgs = args
    lastThis = this
    if (remaining <= 0 || remaining > wait) {
      if (timerId !== null) clearTimeout(timerId)
      timerId = null
      invoke()
    } else if (timerId === null && trailing) {
      timerId = setTimeout(trailingCall, remaining)
    }
    return result
  }
  throttled.cancel = () => {
    if (timerId !== null) clearTimeout(timerId)
    timerId = null
    lastInvoke = 0
    lastArgs = undefined
    lastThis = undefined
  }
  throttled.flush = () => {
    if (timerId === null) return result
    clearTimeout(timerId)
    trailingCall()
    return result
  }
  return throttled
}

export default throttle