前端 js 日常开发中,在页面鼠标移动 mousemove,或窗口的 resize、scroll,输入框内容校验等高频操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,如果处理不当或放任不管很容易引起浏览器卡死,导致用户体验非常差。此时我们可以采用 debounce(防抖)和 throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
一、 函数防抖
函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次;如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
以鼠标移动为例,当鼠标移动时,计算鼠标当前的位置。通常情况下,我们的代码如下:
<script> function handleMousemove(args) { console.log("鼠标位置,X:" + args.clientX + ",Y: " + args.clientY); } document.body.onmousemove = handleMousemove; </script>
实际运行时,我们会发现,只要鼠标移动,控制台就会不停的输出结果。这样的执行频率太高了,然而我们并不需要这么高的执行频率。此时,就可以使用防抖来减少无意义的执行。
思路如下:在第一次触发事件时,不会立即执行处理函数,而是给出一个延时期限(如1秒)。如果在1秒延时内,没有再次触发事件,则执行处理函数;如果在1秒延时时间内,再次触发事件,则取消当前的计时,重新开始计时。这样,在短时间内大量触发同一事件,最后只会执行一次。
具体示例代码如下(用到js闭包和setTimeout):
<script> var args = null; // 防抖 // fn : 需要防抖的函数 // delay : 毫秒,防抖计时 function debounce(fn, delay) { var timer = null; return function () { args = arguments[0]; if (timer) { // 进入该分支,说明有一个正在计时的处理,然后又再次触发了相同的事件 clearTimeout(timer); } timer = setTimeout(fn, delay); // 重新开始计时(若指定时间delay内没有再次触发相同事件,将执行处理函数fn) } } function handleMousemove() { console.log("鼠标位置,X:" + args.clientX + ",Y: " + args.clientY); } document.body.onmousemove = debounce(handleMousemove, 1000); </script>
运行以上代码,当鼠标持续移动,触发 mousemove 事件时,事件处理函数 handleMousemove 只在停止移动1000毫秒之后才会调用一次;而在持续移动鼠标,触发 mousemove 事件的过程中,事件处理函数 handleMousemove 一直没有执行。
对于函数防抖,如果事件一直持续触发(如鼠标在屏幕上不停移动),只要不停止触发,理论上是不会执行处理函数。不过,如果想要在事件触发的过程中,每隔固定的时间,执行一次事件处理函数,就要用到函数节流。
二、函数节流
函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如给草坪浇水,阀门一打开,就开始放水;不过我们不能一直让水留着,最好是按照固定的时间间隔(如1天)自动开关。
函数节流主要有两种实现方法:时间戳和定时器。
接下来仍以鼠标移动为例,每隔1秒,计算鼠标当前位置。分别用两种方法实现节流。
2.1 节流throttle代码(时间戳):
<script> // 节流 // fn : 需要节流的函数 // delay : 毫秒,执行事件处理间隔 function throttle(fn, delay) { var lastTime = Date.now(); return function () { var args = arguments[0]; var curTime = Date.now(); if (curTime - lastTime >= delay) { fn(args); lastTime = curTime; } } } function handleMousemove(args) { console.log("鼠标位置,X:" + args.clientX + ",Y: " + args.clientY); } document.body.onmousemove = throttle(handleMousemove, 2000); </script>
以上代码,当 mousemove 事件触发时,在超过指定时间间隔(2秒)后再次触发事件,会执行第一次事件处理函数;而后如果持续频繁地触发事件,也都是每超过delay时间(2秒)执行一次。
2.2 节流throttle代码(定时器):
<script> // 节流 // fn : 需要节流的函数 // delay : 毫秒,执行事件处理间隔 function throttle(fn, delay) { var timer = null; return function () { var args = arguments[0]; if (!timer) { timer = setTimeout(function () { fn(args); timer = null; }, delay); } } } function handleMousemove(args) { console.log("鼠标位置,X:" + args.clientX + ",Y: " + args.clientY); } document.body.onmousemove = throttle(handleMousemove, 2000); </script>
以上代码,当触发事件的时候,我们设置一个定时器,直到delay时间(2秒)后,定时器执行事件处理函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay秒(2秒)后才执行。而后如果持续频繁地触发事件,也都是每delay时间(2秒)执行一次。当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次事件处理函数。
总结
函数防抖:将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是如果在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。