前端性能优化利器——深入理解防抖(Debounce)

547 阅读6分钟

告别卡顿!前端性能优化利器——深入理解防抖(Debounce)

引言

在前端开发中,我们经常会遇到一些需要频繁触发的事件,例如窗口的resize、输入框的input、鼠标的mousemove等。如果这些事件的回调函数执行的开销较大,频繁触发就可能导致页面卡顿,严重影响用户体验。为了解决这个问题,前端性能优化中引入了“防抖”(Debounce)和“节流”(Throttle)这两个重要的概念。本文将深入探讨防抖的原理、应用场景以及如何手写一个高质量的防抖函数,帮助你告别页面卡顿,写出更流畅、更高效的前端代码。

什么是防抖?

防抖,顾名思义,就是防止抖动。在前端领域,它指的是在事件被触发后,不立即执行回调函数,而是等待一段时间,如果在这段时间内事件再次被触发,则重新计时。直到事件在指定的时间内不再被触发,才执行回调函数。这就像我们乘坐电梯,只有当电梯门关闭后,等待一段时间,如果没有人再按开门键,电梯才会开始运行。如果在等待期间有人按了开门键,电梯门会重新打开,并重新计时。

防抖的核心思想是:在一定时间内,只执行最后一次操作。

防抖的应用场景

防抖在前端开发中有着广泛的应用,以下是一些常见的场景:

  • 搜索框实时搜索:用户在搜索框中输入内容时,我们通常不希望每输入一个字符就立即发送一次请求。通过防抖,可以实现在用户停止输入一段时间后才发送搜索请求,减少不必要的网络请求,减轻服务器压力。
  • 窗口resize事件:当浏览器窗口大小改变时,resize事件会频繁触发。如果resize的回调函数中包含复杂的DOM操作或计算,会导致性能问题。使用防抖可以确保在窗口大小调整完成后才执行相关操作。
  • 表单提交:防止用户在短时间内多次点击提交按钮,导致重复提交表单或创建多条数据。通过防抖,可以确保在用户点击提交后,在一定时间内不再响应后续的点击事件。
  • 滚动加载:在页面滚动到底部时加载更多内容,如果滚动事件没有防抖处理,可能会导致频繁触发加载,影响性能。防抖可以确保在用户停止滚动一段时间后才触发加载。

如何手写一个高质量的防抖函数

理解了防抖的原理和应用场景,接下来我们来手写一个高质量的防抖函数。一个高质量的防抖函数需要考虑以下几个方面:

  1. 闭包保存定时器:利用闭包的特性,将定时器timer保存在函数作用域中,确保每次触发事件时都能访问到同一个timer
  2. 清除上一次的定时器:每次事件触发时,都需要清除上一次设置的定时器,然后重新设置一个新的定时器,这样才能保证只有在指定时间内没有再次触发事件时,回调函数才会被执行。
  3. 参数传递:确保防抖函数内部的回调函数能够正确接收到外部传入的参数。
  4. this指向问题:在JavaScript中,this的指向是一个常见的问题。我们需要确保防抖函数内部的回调函数在执行时,this的指向与原始函数的this指向一致。

下面是一个基础的防抖函数实现:

 function debounce(func, delay) {
   let timer = null;
   return function(...args) {
     if (timer) {
       clearTimeout(timer);
     }
     timer = setTimeout(() => {
       func.apply(this, args);
     }, delay);
   };
 }

代码解析:

  • debounce函数接收两个参数:func(需要防抖的函数)和delay(延迟时间,单位毫秒)。
  • timer变量用于存储定时器ID,初始值为null
  • 返回一个匿名函数,这个匿名函数才是真正绑定到事件上的函数。
  • 在匿名函数内部,首先判断timer是否存在,如果存在则清除上一次的定时器,确保每次触发都会重新计时。
  • 然后设置一个新的定时器,在delay时间后执行func.apply(this, args)apply方法用于改变functhis指向,并传递参数。this指向的是调用防抖函数的上下文,args是传入的参数。

立即执行的防抖(Leading Edge)

在某些场景下,我们可能希望事件触发时立即执行一次回调函数,然后才开始防抖,在防抖期间不再执行,直到防抖结束后才能再次立即执行。这被称为“立即执行的防抖”或“前缘防抖”。例如,在点击按钮时,我们希望第一次点击立即响应,而后续的快速点击则被忽略。

 function debounce(func, delay, immediate = false) {
   let timer = null;
   let result;
 ​
   return function(...args) {
     if (timer) {
       clearTimeout(timer);
     }
 ​
     if (immediate && !timer) {
       result = func.apply(this, args);
     }
 ​
     timer = setTimeout(() => {
       if (!immediate) {
         result = func.apply(this, args);
       }
       timer = null;
     }, delay);
 ​
     return result;
   };
 }

代码解析:

  • 新增immediate参数,默认为false,表示是否立即执行。
  • 新增result变量,用于存储函数执行结果。
  • 在定时器设置之前,如果immediatetruetimernull(表示是第一次触发),则立即执行func,并保存结果。
  • 在定时器回调中,如果immediatefalse,则执行func。无论是否立即执行,定时器执行完毕后将timer重置为null,以便下一次触发时可以再次立即执行(如果immediatetrue)。
  • 返回result,使得防抖函数可以获取到原始函数的返回值。

防抖与节流的区别

防抖和节流都是控制函数执行频率的手段,但它们的应用场景和实现原理有所不同:

特性防抖(Debounce)节流(Throttle)
核心思想在一定时间内,只执行最后一次操作。在一定时间内,只执行一次操作。
触发时机事件停止触发后,延迟一段时间执行。事件触发后,立即执行一次,然后每隔一段时间执行一次。
应用场景搜索框输入、窗口resize、表单提交、滚动加载等。游戏射击、高频点击、拖拽事件、动画等。

简单来说,防抖是“有新动作就重新计时,只执行最后一次”,而节流是“固定时间内只执行一次”。

总结

防抖是前端性能优化中一个非常实用的技术,它能够有效减少不必要的函数执行,提升页面性能和用户体验。通过本文的讲解,相信你已经对防抖的原理、应用场景以及如何手写一个高质量的防抖函数有了深入的理解。在实际开发中,合理运用防抖和节流,将帮助你写出更健壮、更高效的前端应用。