防抖与节流
先理解原理和示例,再练习完整口述与追问。
回答要点
触发频率、this 与参数、首尾调用、取消与刷新。
原理拆解
防抖的核心状态是最近参数、接收者与等待计时器;每次触发重置尾调用。节流则定义时间窗口或下一次允许执行时刻,并决定是否补最后一次。leading/trailing 不同组合会改变单次调用和持续调用的行为,需要先讲契约再实现。
示例与推演
按 0、50、100、500ms 四次触发手算输出:尾部防抖只在安静窗口后执行最近参数,首尾节流可能先执行第一次并在窗口尾补最近一次。再验证 cancel 后不会执行,flush 是否立即处理待执行任务,执行前清理参数能否避免长时间保留大对象。
易错点
时间精度不保证严格固定;异步回调返回值和并发是否由工具管理需说明。原题代码给出了具体实现,不应只背“一个等停一个限频”。
参考回答可直接练习口述
防抖不断推迟执行,直到连续操作安静下来;节流在持续触发期间限制执行频率。防抖适合输入搜索,节流适合滚动处理。下面防抖默认只保留尾调用,可配置首调用;节流默认首调用加尾调用。它们都保留调用者的 this 和最后参数,并支持 cancel。它们只控制回调触发,不能解决已发请求的响应乱序。
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait 不合法')
let timer = null
let args
let context
let result
function invoke() {
const currentArgs = args
const currentContext = context
args = context = undefined
result = Reflect.apply(fn, currentContext, currentArgs)
return result
}
function wrapped(...nextArgs) {
const first = timer === null
args = nextArgs
context = this
if (timer !== null) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
if (trailing && args) invoke()
else args = context = undefined
}, wait)
if (leading && first) invoke()
return result
}
wrapped.cancel = () => {
if (timer !== null) clearTimeout(timer)
timer = null
args = context = undefined
}
wrapped.flush = () => {
if (timer !== null) clearTimeout(timer)
timer = null
if (trailing && args) return invoke()
args = context = undefined
return result
}
return wrapped
}
function throttle(fn, wait, { leading = true, trailing = true } = {}) {
if (typeof fn !== 'function') throw new TypeError('fn 必须是函数')
if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait 不合法')
let timer = null
let lastTime = null
let args
let context
let result
function invoke(time) {
lastTime = time
const currentArgs = args
const currentContext = context
args = context = undefined
result = Reflect.apply(fn, currentContext, currentArgs)
return result
}
function later() {
timer = null
if (trailing && args) {
invoke(Date.now())
if (!leading) lastTime = null
} else {
args = context = undefined
if (!leading) lastTime = null
}
}
function wrapped(...nextArgs) {
const now = Date.now()
if (!leading && !trailing) return result
args = nextArgs
context = this
if (lastTime === null) {
if (leading) return invoke(now)
lastTime = now
}
const remaining = wait - (now - lastTime)
if (remaining <= 0 || remaining > wait) {
if (timer !== null) clearTimeout(timer)
timer = null
return invoke(now)
}
if (trailing && timer === null) timer = setTimeout(later, remaining)
if (!trailing) args = context = undefined
return result
}
wrapped.cancel = () => {
if (timer !== null) clearTimeout(timer)
timer = null
lastTime = null
args = context = undefined
}
return wrapped
}
示例:debounce(search, 300) 等最后一次输入后执行;throttle(onScroll, 100) 限制频率。防抖的 leading:true,trailing:true 在一个窗口内仅调用一次时不会重复尾调用;有后续触发才执行尾调用。防抖返回最近实际执行结果,不是“未来结果 Promise”。时间与额外状态均为每次调用 O(1)。