引言
假设你正在开发一个搜索框,用户每输入一个字符就触发搜索请求,这会导致什么后果? 又或者你给窗口滚动事件绑定了复杂的计算逻辑,用户快速滚动页面时会发生什么?
没错,说的就是性能问题!这时候就需要我们的两大护法——**节流(throttle)和防抖(debounce)**出场了。
定义
防抖(debounce):事件触发后,等待n秒再执行回调。如果在这n秒内事件又被触发,则重新计时。就像电梯关门,如果有人进来,门会重新打开等待。
节流(throttle):事件触发后,在n秒内只执行一次回调。就像水龙头,不管你开多大,它都按固定速率流水。
核心
控制事件触发的频率
代码怎么写?
防抖实现思路
- 需要一个定时器
- 每次触发事件时清除之前的定时器
- 重新设置新的定时器
function debounce(fn, delay) {
let timer = null
return function() {
clearTimeout(timer) // 清除之前的定时器
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
节流实现思路
- 需要一个标志位记录是否可执行
- 第一次触发时立即执行
- 执行后设置冷却时间
- 冷却结束后重置标志位
function throttle(fn, delay) {
let canRun = true
return function() {
if (!canRun) return
canRun = false
fn.apply(this, arguments)
setTimeout(() => {
canRun = true
}, delay)
}
}
测试一下
// 防抖测试
const debouncedFn = debounce(() => {
console.log('防抖执行')
}, 1000)
// 快速连续调用
debouncedFn()
debouncedFn()
debouncedFn()
// 只有最后一次调用后1秒才会执行
// 节流测试
const throttledFn = throttle(() => {
console.log('节流执行')
}, 1000)
// 快速连续调用
throttledFn()
throttledFn()
throttledFn()
// 第一次立即执行,之后每隔1秒最多执行一次
进阶优化
带立即执行选项的防抖
有时候我们希望第一次触发立即执行,之后再防抖
function debounce(fn, delay, immediate = false) {
let timer = null
return function() {
if (timer) clearTimeout(timer)
if (immediate && !timer) {
fn.apply(this, arguments)
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, arguments)
}
timer = null
}, delay)
}
}
带取消功能的节流
有时候我们需要手动取消节流
function throttle(fn, delay) {
let canRun = true
let timer = null
const throttled = function() {
if (!canRun) return
canRun = false
fn.apply(this, arguments)
timer = setTimeout(() => {
canRun = true
}, delay)
}
throttled.cancel = function() {
clearTimeout(timer)
canRun = true
}
return throttled
}
实际应用场景
防抖适用场景
- 搜索框输入联想(等待用户停止输入后再请求)
- 窗口大小调整(等待调整结束后再计算布局)
- 表单验证(用户停止输入后再验证)
节流适用场景
- 滚动加载更多(固定间隔检查位置)
- 按钮连续点击(防止重复提交)
- 鼠标移动事件(降低事件触发频率)
面试官的鼻子怎么牵?
假设面试官问:请你聊聊前端性能优化
可以这么回答: "在前端开发中,事件处理函数的频繁调用会导致性能问题。比如搜索框的实时搜索、窗口的滚动事件等。这时候我们可以使用节流和防抖来优化性能。"
"防抖就像电梯关门,如果有人进来就重新计时;节流就像水龙头,不管开多大都按固定速率流水。"
"在React/Vue中,我们可以用lodash的debounce/throttle,或者自己实现一个..."
你都这么聊了,面试官当然要考考你:请你手写一个防抖/节流函数
这个时候,你不就可以给他露一手了吗?
总结对比
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 执行时机 | 事件停止触发后执行 | 固定时间间隔执行 |
| 执行次数 | 只执行最后一次 | 均匀执行 |
| 适用场景 | 搜索联想、窗口resize | 滚动加载、按钮防重复点击 |
| 类比 | 电梯关门 | 水龙头流水 |
手写完整版
// 完整版防抖
function debounce(fn, delay, immediate = false) {
let timer = null
let isInvoked = false
function debounced(...args) {
if (timer) clearTimeout(timer)
if (immediate && !isInvoked) {
fn.apply(this, args)
isInvoked = true
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args)
}
timer = null
isInvoked = false
}, delay)
}
debounced.cancel = function() {
clearTimeout(timer)
timer = null
isInvoked = false
}
return debounced
}
// 完整版节流
function throttle(fn, delay, options = { leading: true, trailing: true }) {
let timer = null
let lastTime = 0
function throttled(...args) {
const now = Date.now()
// 第一次不立即执行
if (!lastTime && !options.leading) {
lastTime = now
}
const remaining = delay - (now - lastTime)
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer)
timer = null
}
fn.apply(this, args)
lastTime = now
} else if (!timer && options.trailing) {
timer = setTimeout(() => {
fn.apply(this, args)
lastTime = options.leading ? Date.now() : 0
timer = null
}, remaining)
}
}
throttled.cancel = function() {
clearTimeout(timer)
timer = null
lastTime = 0
}
return throttled
}
现在,你已经掌握了节流和防抖的精髓,下次面试官问到这个问题时,你就可以从容应对了!