首先我们来了解下什么是Throttle
1. 定义
如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。
也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。
接口定义:
* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay * @param delay {number} 延迟时间,单位毫秒 * @param action {function} 请求关联函数,实际应用需要调用的函数 * @return {function} 返回客户调用函数 */ throttle(delay,action)
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 变量对外是不可见的,但是内部延时函数触发时还可以访问到 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 重置成当前时间,简单来说就是:相对于下一次的上一次其实就是当前