0基础进大厂,第20天——面试官:请你聊聊防抖和节流

58 阅读4分钟

引言

假设你正在开发一个搜索框,用户每输入一个字符就触发搜索请求,这会导致什么后果? 又或者你给窗口滚动事件绑定了复杂的计算逻辑,用户快速滚动页面时会发生什么?

没错,说的就是性能问题!这时候就需要我们的两大护法——**节流(throttle)防抖(debounce)**出场了。

定义

防抖(debounce):事件触发后,等待n秒再执行回调。如果在这n秒内事件又被触发,则重新计时。就像电梯关门,如果有人进来,门会重新打开等待。

节流(throttle):事件触发后,在n秒内只执行一次回调。就像水龙头,不管你开多大,它都按固定速率流水。

核心

控制事件触发的频率

代码怎么写?

防抖实现思路

  1. 需要一个定时器
  2. 每次触发事件时清除之前的定时器
  3. 重新设置新的定时器
function debounce(fn, delay) {
  let timer = null
  return function() {
    clearTimeout(timer) // 清除之前的定时器
    timer = setTimeout(() => {
      fn.apply(this, arguments)
    }, delay)
  }
}

节流实现思路

  1. 需要一个标志位记录是否可执行
  2. 第一次触发时立即执行
  3. 执行后设置冷却时间
  4. 冷却结束后重置标志位
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
}

实际应用场景

防抖适用场景

  1. 搜索框输入联想(等待用户停止输入后再请求)
  2. 窗口大小调整(等待调整结束后再计算布局)
  3. 表单验证(用户停止输入后再验证)

节流适用场景

  1. 滚动加载更多(固定间隔检查位置)
  2. 按钮连续点击(防止重复提交)
  3. 鼠标移动事件(降低事件触发频率)

面试官的鼻子怎么牵?

假设面试官问:请你聊聊前端性能优化

可以这么回答: "在前端开发中,事件处理函数的频繁调用会导致性能问题。比如搜索框的实时搜索、窗口的滚动事件等。这时候我们可以使用节流和防抖来优化性能。"

"防抖就像电梯关门,如果有人进来就重新计时;节流就像水龙头,不管开多大都按固定速率流水。"

"在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
}

现在,你已经掌握了节流和防抖的精髓,下次面试官问到这个问题时,你就可以从容应对了!