详解JavaScript节流函数中的Throttle

作者:纸飞机

时间:2026-10-30

浏览:438

本文深入解析JavaScript中的Throttle(节流)函数,通过对比错误实现与闭包优化方案,详细阐述其核心原理。文章结合resize和scroll事件场景,演示如何通过记录时间戳实现精确控制,并探讨atleast参数对触发频率的影响,帮助开发者有效优化高频事件带来的性能问题。

文章目录

Throttle 定义与原理

首先我们来了解Throttle(节流)的概念。

1. 定义

想象将水龙头拧紧至水滴状,你会发现水以固定间隔滴落。

这对应于预先设定一个执行周期:当调用动作的时刻大于等于执行周期时,则执行该动作,随后进入下一个新周期。

接口定义:


* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay
* @param delay {number} 延迟时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
throttle(delay,action)

Throttle 简单实现与优化

2. 简单实现


var throttle = function(delay, action){
 var last = 0return function(){
 var curr = +new Date()
 if (curr - last > delay){
  action.apply(this, arguments)
  last = curr 
 }
 }
}

下面详细解释这个节流函数的实现过程。

在浏览器DOM事件中,部分事件会随用户操作频繁触发,例如调整浏览器窗口大小(resize)、页面滚动(scroll)和鼠标移动(mousemove)。当用户触发这些操作且绑定了相应事件处理方法时,该方法会不断被调用。

这并非理想状态。若事件处理方法逻辑复杂或涉及大量DOM操作,频繁触发会导致性能损耗,进而降低用户体验(如UI响应迟缓、浏览器卡顿)。因此,通常需为相关事件添加延迟执行逻辑。

通常使用以下代码实现该功能:


var COUNT = 0;
function testFn() { console.log(COUNT++); }
// 浏览器resize的时候
// 1. 清除之前的计时器
// 2. 添加一个计时器让真正的函数testFn延后100毫秒触发
window.onresize = function () {
 var timer = null;
 clearTimeout(timer);
 
 timer = setTimeout(function() {
  testFn();
 }, 100);
};

细心的读者会发现上述代码存在错误,这是新手常犯的问题:setTimeout函数的返回值应保存在一个相对全局的变量中,否则每次resize都会创建新的计时器,无法达到预期效果。

因此我们修改了代码:


var timer = null;
window.onresize = function () {
 clearTimeout(timer);
 timer = setTimeout(function() {
  testFn();
 }, 100);
};

此时代码运行正常,但引入了一个新问题——产生了全局变量timer。若页面其他功能也使用名为timer的变量,不同代码间可能产生冲突。为解决此问题,需利用JavaScript的闭包(closures)特性。相关知识可查阅MDN文档,改造后的代码如下:


/**
 * 函数节流方法
 * @param Function fn 延时调用函数
 * @param Number delay 延迟多长时间
 * @return Function 延迟执行的方法
 */
var throttle = function (fn, delay) {
 var timer = null;
 
 return function () {
  clearTimeout(timer);
  timer = setTimeout(function() {
   fn();
  }, delay);
 }
};
window.onresize = throttle(testFn, 200, 1000);

我们使用一个闭包函数(throttle节流)将timer置于内部并返回延时处理函数。这样,timer变量对外不可见,但内部延时函数触发时仍可访问该变量。

当然,这种写法对新手而言可能较难理解,我们可以变换一种写法来辅助理解:


var throttle = function (fn, delay) {
 var timer = null;
 
 return function () {
  clearTimeout(timer);
  timer = setTimeout(function() {
   fn();
  }, delay);
 }
};
 
var f = throttle(testFn, 200);
window.onresize = function () {
 f();
};

这里主要了解一点:throttle被调用后返回的function才是onresize触发时需要调用的函数。

现在看起来该方法已接近完美,然而实际使用中并非如此。举个例子:

如果用户不断resize浏览器窗口大小,此时延迟处理函数一次都不会执行。

于是我们又要添加一个功能:当用户触发resize的时候应该 在某段时间 内至少触发一次,既然是在某段时间内,那么这个判断条件就可以取当前的时间毫秒数,每次函数调用把当前的时间和上一次调用时间相减,然后判断差值如果大于 某段时间 就直接触发,否则还是走 timeout 的延迟逻辑。

下面的代码里面需要指出的是:

previous 变量的作用和 timer 类似,都是记录上一次的标识,必须是相对的全局变量 如果逻辑流程走的是“至少触发一次”的逻辑,那么函数调用完成需要把 previous 重置成当前时间,简单来说就是:相对于下一次的上一次其实就是当前


/**
 * 函数节流方法
 * @param Function fn 延时调用函数
 * @param Number delay 延迟多长时间
 * @param Number atleast 至少多长时间触发一次
 * @return Function 延迟执行的方法
 */
var throttle = function (fn, delay, atleast) {
 var timer = null;
 var previous = null;
 
 return function () {
  var now = +new Date();
 
  if ( !previous ) previous = now;
 
  if ( now - previous > atleast ) {
   fn();
   // 重置上一次开始时间为本次结束时间
   previous = now;
  } else {
   clearTimeout(timer);
   timer = setTimeout(function() {
    fn();
   }, delay);
  }
 }
};

Throttle 实践与测试

实践:

我们模拟一个窗口 scroll 时节流的场景,也就是说当用户滚动页面向下的时候我们需要节流执行一些方法,比如:计算 DOM 位置等需要连续操作 DOM 元素的动作

完整代码如下:


<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>throttle</title>
</head>
<body>
 <div style="height:5000px">
  <div id="demo" style="position:fixed;"></div>
 </div>
 <script>
 var COUNT = 0, demo = document.getElementById('demo');
 function testFn() {demo.innerHTML += 'testFN 被调用了 ' + ++COUNT + '次<br>';}
 
 var throttle = function (fn, delay, atleast) {
  var timer = null;
  var previous = null;
 
  return function () {
   var now = +new Date();
 
   if ( !previous ) previous = now;
   if ( atleast && now - previous > atleast ) {
    fn();
    // 重置上一次开始时间为本次结束时间
    previous = now;
    clearTimeout(timer);
   } else {
    clearTimeout(timer);
    timer = setTimeout(function() {
     fn();
     previous = null;
    }, delay);
   }
  }
 };
 window.onscroll = throttle(testFn, 200);
 // window.onscroll = throttle(testFn, 500, 1000);
 </script>
</body>
</html>

我们用两个 case 来测试效果,分别是添加至少触发 atleast 参数和不添加:


// case 1
window.onscroll = throttle(testFn, 200);
// case 2
window.onscroll = throttle(testFn, 200, 500);

case 1 的表现为:在页面滚动的过程(不能停止)中 testFN 不会被调用,直到停止的时候会调用一次,也就是说执行的是 throttle 里面 最后 一个 setTimeout ,效果如图:

 Throttle 定义与原理

case 2 的表现为:在页面滚动的过程(不能停止)中 testFN 第一次会延迟 500ms 执行(来自至少延迟逻辑),后来至少每隔 500ms 执行一次,效果如图

 Throttle 定义与原理

如上展示,我们要实现的效果已经介绍完毕并奉上了示例,望对有需要的朋友有所帮助。后续的一些辅助性优化读者可以自己琢磨,如:函数 this 指向,返回值保存等。总之仔仔细细理解一下这个过程感觉真好!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章