JavaScript防抖与节流函数怎么写?高频事件优化技巧详解

原创 2025-08-08 09:39:16编程技术
453

在前端开发中,滚动、输入等高频事件若直接绑定回调,常导致页面卡顿。此时,防抖与节流是关键的性能优化手段——防抖“等用户停手再执行”,节流“按固定间隔强制执行”。本文ZHANID工具网将详细解析防抖与节流函数的自定义实现逻辑,结合滚动、输入等高频场景,教你用这两项技术轻松解决性能痛点,提升页面流畅度。

一、为什么需要防抖与节流?

在Web开发中,我们经常需要处理高频触发的事件,例如:

  • 窗口的resize事件

  • 滚动条的scroll事件

  • 输入框的inputkeyup事件

  • 鼠标移动的mousemove事件

高频事件直接绑定处理函数会导致性能问题:假设我们有一个搜索框,每次输入都触发AJax请求,用户快速输入"javascript"时,会触发8次请求(包括删除操作),这不仅浪费服务器资源,还会造成页面卡顿。

防抖(debounce)和节流(throttle)是两种优化高频事件的技术方案,它们通过限制函数执行频率来提升性能。

二、防抖(Debounce)原理与实现

1. 防抖的核心思想

防抖是指触发事件后,在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间。就像电梯关门:

  • 电梯不会立即关门,而是等待一段时间(比如10秒)

  • 如果这段时间内又有人进入,则重新计时

  • 只有超时后无人进入,电梯才会关门

2. 基础实现

function debounce(fn, delay) {
 let timer = null;
 return function(...args) {
  // 清除之前的定时器
  if (timer) clearTimeout(timer);
  // 设置新的定时器
  timer = setTimeout(() => {
   fn.apply(this, args);
  }, delay);
 };
}

3. 立即执行版防抖

有时我们希望第一次触发立即执行,之后停止触发n秒后才允许再次执行

function debounceImmediate(fn, delay) {
 let timer = null;
 return function(...args) {
  if (!timer) {
   fn.apply(this, args); // 立即执行
  }
  // 清除之前的定时器
  clearTimeout(timer);
  // 设置新的定时器
  timer = setTimeout(() => {
   timer = null; // 执行后清除timer
  }, delay);
 };
}

4. 防抖应用场景

  • 搜索框输入联想(用户停止输入后发送请求)

  • 窗口大小调整(resize结束时计算布局)

  • 表单验证(用户停止输入后验证)

  • 按钮连续点击防护(防止重复提交)

5. 防抖实现要点

  • 定时器管理:每次触发事件时清除之前的定时器

  • 上下文保持:使用apply确保回调函数中的this指向正确

  • 参数传递:通过...args收集所有参数并传递给回调函数

  • 返回值处理:如果需要返回值,可以返回一个Promise或添加回调

三、节流(Throttle)原理与实现

1. 节流的核心思想

节流是指连续触发事件但在n秒中只执行一次函数。就像水龙头:

  • 水龙头不会一直流水,而是间隔一段时间(比如5秒)流一次

  • 无论你如何频繁地开关水龙头,水流速度都是恒定的

2. 时间戳版实现

function throttle(fn, delay) {
 let lastTime = 0;
 return function(...args) {
  const now = Date.now();
  if (now - lastTime >= delay) {
   fn.apply(this, args);
   lastTime = now;
  }
 };
}

3. 定时器版实现

function throttleTimer(fn, delay) {
 let timer = null;
 return function(...args) {
  if (!timer) {
   timer = setTimeout(() => {
    fn.apply(this, args);
    timer = null; // 执行后清除timer
   }, delay);
  }
 };
}

4. 混合版实现(推荐)

结合时间戳和定时器的优点,首次立即执行,最后一次也会执行

function throttleHybrid(fn, delay) {
 let lastTime = 0;
 let timer = null;
 return function(...args) {
  const now = Date.now();
  const remaining = delay - (now - lastTime);
  
  if (remaining <= 0) {
   // 超过延迟时间,立即执行
   if (timer) {
    clearTimeout(timer);
    timer = null;
   }
   fn.apply(this, args);
   lastTime = now;
  } else if (!timer) {
   // 设置最后一次执行的定时器
   timer = setTimeout(() => {
    fn.apply(this, args);
    lastTime = Date.now();
    timer = null;
   }, remaining);
  }
 };
}

5. 节流应用场景

  • 滚动加载更多(scroll事件)

  • 鼠标移动事件(mousemove)

  • 游戏中的实时计算(如角色移动)

  • DOM元素的拖拽(drag)

  • 射击游戏中的连续射击(限制发射频率)

6. 节流实现要点

  • 时间控制:确保函数执行间隔不小于设定值

  • 首次执行:根据需求决定是否立即执行

  • 末次执行:确保停止触发后能执行最后一次

  • 性能优化:定时器版比时间戳版性能更好(少比较操作)

四、防抖与节流的对比

特性 防抖(Debounce) 节流(Throttle)
执行时机 停止触发后延迟执行 固定时间间隔执行
执行次数 0或1次(取决于触发间隔) 多次(按固定频率)
类似场景 电梯关门 水龙头滴水
内存管理 需要清除定时器 需要清除定时器
参数传递 需要保留所有触发参数 需要保留所有触发参数

选择建议

  • 需要等待用户停止操作后执行 → 防抖

  • 需要均匀间隔执行 → 节流

  • 既需要首次响应又需要限制频率 → 混合版节流

五、高级实现技巧

1. 添加取消功能

function debounceCancelable(fn, delay) {
 let timer = null;
 const debounced = function(...args) {
  if (timer) clearTimeout(timer);
  timer = setTimeout(() => {
   fn.apply(this, args);
  }, delay);
 };
 
 debounced.cancel = function() {
  if (timer) {
   clearTimeout(timer);
   timer = null;
  }
 };
 
 return debounced;
}

// 使用示例
const myDebounce = debounceCancelable(() => console.log('Debounced!'), 500);
window.addEventListener('resize', myDebounce);
// 需要取消时
// myDebounce.cancel();

2. 添加立即执行选项

function debounceWithOptions(fn, delay, options = {}) {
 let timer = null;
 const { leading = false, trailing = true } = options;
 let lastArgs = null;
 let lastThis = null;
 
 function invoke() {
  fn.apply(lastThis, lastArgs);
  timer = null;
 }
 
 return function(...args) {
  lastArgs = args;
  lastThis = this;
  
  if (timer && !leading) {
   clearTimeout(timer);
  }
  
  const shouldInvokeNow = leading && !timer;
  
  if (shouldInvokeNow) {
   invoke();
  } else if (trailing) {
   timer = setTimeout(invoke, delay);
  }
 };
}

3. 节流的leading/trailing控制

function throttleAdvanced(fn, delay, options = {}) {
 let lastTime = 0;
 let timer = null;
 const { leading = true, trailing = true } = options;
 
 return function(...args) {
  const now = Date.now();
  const shouldInvokeNow = leading && (now - lastTime >= delay);
  const remaining = delay - (now - lastTime);
  
  if (shouldInvokeNow) {
   lastTime = now;
   fn.apply(this, args);
  } else if (trailing && !timer) {
   timer = setTimeout(() => {
    lastTime = Date.now();
    timer = null;
    fn.apply(this, args);
   }, remaining);
  }
 };
}

JavaScript.webp

六、实际项目中的应用

1. 搜索框防抖

class SearchBox {
 constructor() {
  this.input = document.getElementById('search');
  this.debouncedSearch = this.debounce(this.fetchResults, 300);
  this.init();
 }
 
 init() {
  this.input.addEventListener('input', (e) => {
   this.debouncedSearch(e.target.value);
  });
 }
 
 debounce(fn, delay) {
  let timer = null;
  return function(...args) {
   if (timer) clearTimeout(timer);
   timer = setTimeout(() => {
    fn.apply(this, args);
   }, delay);
  };
 }
 
 fetchResults(query) {
  if (!query.trim()) return;
  console.log(`Fetching results for: ${query}`);
  // 实际项目中这里会是fetch/axios请求
 }
}

new SearchBox();

2. 滚动加载节流

class InfiniteScroll {
 constructor() {
  this.container = document.getElementById('container');
  this.throttledScroll = this.throttle(this.handleScroll, 200);
  this.init();
 }
 
 init() {
  window.addEventListener('scroll', this.throttledScroll);
 }
 
 throttle(fn, delay) {
  let lastTime = 0;
  return function(...args) {
   const now = Date.now();
   if (now - lastTime >= delay) {
    fn.apply(this, args);
    lastTime = now;
   }
  };
 }
 
 handleScroll() {
  const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
  if (scrollTop + clientHeight >= scrollHeight - 50) {
   console.log('Loading more items...');
   // 实际项目中这里会加载更多数据
  }
 }
}

new InfiniteScroll();

3. 表单验证混合方案

class FormValidator {
 constructor() {
  this.form = document.getElementById('myForm');
  this.usernameInput = document.getElementById('username');
  this.debouncedValidate = this.debounce(this.validateUsername, 500);
  this.throttledKeyup = this.throttle(this.onKeyup, 200);
  this.init();
 }
 
 init() {
  this.usernameInput.addEventListener('input', this.throttledKeyup);
  this.usernameInput.addEventListener('blur', this.debouncedValidate);
 }
 
 debounce(fn, delay) {
  let timer = null;
  return function(...args) {
   if (timer) clearTimeout(timer);
   timer = setTimeout(() => {
    fn.apply(this, args);
   }, delay);
  };
 }
 
 throttle(fn, delay) {
  let lastTime = 0;
  return function(...args) {
   const now = Date.now();
   if (now - lastTime >= delay) {
    fn.apply(this, args);
    lastTime = now;
   }
  };
 }
 
 onKeyup() {
  // 实时反馈(如字符计数)
  const len = this.usernameInput.value.length;
  console.log(`Current length: ${len}`);
 }
 
 validateUsername(value = this.usernameInput.value) {
  // 复杂验证逻辑
  if (!value) {
   console.log('Username is required');
   return false;
  }
  if (value.length < 4) {
   console.log('Username too short');
   return false;
  }
  console.log('Username valid');
  return true;
 }
}

new FormValidator();

七、性能测试与优化

1. 测试代码

// 防抖测试
function testDebounce() {
 console.time('debounce');
 const debouncedFn = debounce(() => {
  console.timeEnd('debounce');
  console.log('Debounced function executed');
 }, 500);
 
 // 快速触发
 for (let i = 0; i < 10; i++) {
  setTimeout(() => debouncedFn(), i * 100);
 }
}

// 节流测试
function testThrottle() {
 console.time('throttle');
 const throttledFn = throttle(() => {
  console.log('Throttled function executed');
 }, 500);
 
 // 持续触发
 const start = Date.now();
 while (Date.now() - start < 2000) {
  throttledFn();
 }
}

// 运行测试
// testDebounce();
// testThrottle();

2. 优化建议

  1. 减少DOM操作:在防抖/节流函数内部尽量减少DOM查询和操作

  2. 避免内存泄漏:及时清除定时器(如在组件卸载时)

  3. 合理设置延迟时间:根据实际需求调整delay参数

  4. 使用requestAnimationFrame:对于动画相关操作,考虑使用rAF替代setTimeout

  5. Web Worker:对于特别耗时的计算,考虑放到Web Worker中

八、常见误区与解决方案

1. this指向问题

错误示例

element.addEventListener('click', debounce(function() {
 console.log(this); // 指向window而非element
}, 500));

解决方案

  • 使用箭头函数(如果不需要动态this)

  • 在防抖函数内部使用apply/call绑定this

  • 使用bind预先绑定this

2. 事件对象传递

错误示例

element.addEventListener('click', debounce(function(e) {
 console.log(e); // e可能是undefined或最后一次的event
}, 500));

解决方案

  • 使用...args收集所有参数

  • 在防抖函数内部正确传递event对象

3. 多次绑定问题

错误示例

// 在resize事件中多次绑定防抖函数
window.addEventListener('resize', debounce(fn, 500));
window.addEventListener('resize', debounce(fn, 500)); // 创建了两个定时器

解决方案

  • 将防抖函数保存在变量中复用

  • 使用单例模式管理防抖函数

九、浏览器原生实现

现代浏览器已经内置了类似功能:

1. lodash的防抖/节流

虽然不是原生,但已成为事实标准:

import { debounce, throttle } from 'lodash';

// 使用方式与自定义实现类似
window.addEventListener('resize', debounce(handleResize, 200));

2. AbortController(取消异步操作)

虽然不是直接替代,但可以用于取消未执行的防抖/节流操作:

const controller = new AbortController();

function debounceWithAbort(fn, delay) {
 let timer = null;
 return function(...args) {
  if (timer) clearTimeout(timer);
  timer = setTimeout(() => {
   if (!controller.signal.aborted) {
    fn.apply(this, args);
   }
  }, delay);
 };
}

// 需要取消时
// controller.abort();

十、总结

  1. 防抖适用于等待用户停止操作后执行的场景,如搜索输入、窗口调整

  2. 节流适用于需要均匀间隔执行的场景,如滚动加载、鼠标移动

  3. 混合方案结合两者优点,能处理更复杂的需求

  4. 实现要点包括定时器管理、上下文保持、参数传递和内存清理

  5. 高级技巧包括添加取消功能、立即执行选项和更精细的控制

  6. 实际应用中需要根据具体场景选择合适的方案和参数

通过合理使用防抖和节流技术,可以显著提升Web应用的性能和用户体验,特别是在处理高频触发的事件时。

JavaScript 防抖 节流
THE END
战地网
频繁记录吧,生活的本意是开心

相关推荐

JavaScript 中 instanceof 的作用及使用方法详解
在 JavaScript 的类型检查体系中,instanceof 是一个重要的操作符,用于判断一个对象是否属于某个构造函数的实例或其原型链上的类型。本文ZHANID工具网将系统讲解 instanceof...
2025-09-11 编程技术
498

JavaScript出现“undefined is not a function”错误的解决方法
在JavaScript开发中,TypeError: undefined is not a function 是最常见的运行时错误之一,通常表示代码尝试调用一个未定义(undefined)的值作为函数。本文ZHANID工具网将从...
2025-09-10 编程技术
512

JavaScript报错“Uncaught ReferenceError”如何解决?
在JavaScript开发中,“Uncaught ReferenceError”是常见且易混淆的错误类型。本文ZHANID工具网从错误本质、常见场景、排查步骤、解决方案四个维度,结合真实代码案例与调试技...
2025-09-09 编程技术
565

JavaScript面试题汇总:高频考点与答案解析
在前端开发领域,JavaScript作为核心语言,其面试题覆盖了从基础语法到高级特性的广泛范围。本文ZHANID工具网将系统梳理JavaScript高频面试考点,结合权威资料与典型案例,为...
2025-09-08 编程技术
476

JavaScript中严格模式(use strict)的作用与使用场景
JavaScript的灵活性既是其优势,也是开发者面临的挑战。非严格模式下,隐式全局变量、模糊的this绑定等特性容易导致难以调试的错误。为解决这些问题,ECMAScript 5(ES5)引入...
2025-09-04 编程技术
532

使用JavaScript开发一个简易计算器(附示例代码)
在Web开发领域,JavaScript因其灵活性和强大的交互能力成为实现动态功能的核心技术。本文ZHANID工具网将通过构建一个简易计算器,系统讲解如何利用HTML、CSS和JavaScript完成...
2025-09-03 编程技术
525