JavaScript 防抖与节流

323 阅读5分钟

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

html 文件中代码如下:

<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
    let num = 1;
    let content = document.getElementById('content');

    function count() {
        content.innerHTML = num++;
    };
    content.onmousemove = count;
</script>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。

可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。

防抖(debounce)

**所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次。如果在 n 秒内又触发了事件,则会重新计算函数执行时间。**相当于以最后一次触发为时间开始计时

再给大家举个生活例子: 如果有人进电梯(触发事件),那电梯将在10s出发(执行事件句柄),这时如果又有人进电梯了(在10s内再次触发该事件),我们又得等10s再出发(重新计时)。这个例子很完美的展示了函数防抖的精髓。

总结:

  • 函数执行过一次后,在等待某时间段内不能再次执行
  • 在等待时间内触发此函数,则重新计算等待时间

防抖函数分为非立即执行版和立即执行版。

非立即执行

非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

function debounce(fn, delay){
    var timer = null; // 声明计时器
    return function() {
        var context = this;
        var args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function () {
            fn.apply(context, args);
        }, delay);
    };
}

立即执行

立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

function debounce(fn, delay){
    var timer = null; // 声明计时器
    return function() {
        var context = this;
        var args = arguments;
        if (timer) {
            clearTimeout(timer)
        }else{
            binding.value(e)
        }
        timer = setTimeout(function () {
            fn.apply(context, args);
        }, delay);
    };
}

完整版

在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来。

  • Vue 指令版本
/**
 * @param immediate true 表立即执行,false 表非立即执行
 */
// 防抖
const debounce = {
	inserted: function(el, binding) {
        let timer
		// binding.arg 为指令参数,指定为什么事件类型,默认为click
		el.addEventListener(binding.arg || 'click', (e) => {
            if (timer) clearTimeout(timer)
			if (binding.modifiers.immediate && !timer) {
				binding.value(e)
			}
			timer = setTimeout(() => {
				binding.value(e)
			}, 500)
		})
	},
}

export default debounce
  • 事件版本
debounce(callback, time = 500, immediate) {
    let timer = null // 定义一个全局变量,通过闭包的方式,返回一个函数,后面事件执行就会执行函数,且拿到timer
    return function(){
        var context = this; // 当触发事件,this指向触发事件的DOM
        var args = arguments; // 事件函数的参数
        // 如果有定时器,清除定时器
        if (timer) {
            clearTimeout(this.timer)
        } else if (immediate && !timer) {
            // 如果没有定时器说明是第一次,并且immediate为真,则立即执行
            callback.apply(context,args)
        }
        timer = setTimeout(() => {
            callback.apply(context,args)
        }, time)
    }
}

使用场景

  • 每次 resize/scroll 触发统计事件
  • 文本输入验证(连续输入文字后发生AJAX请求进行验证,验证一次就好)
  • mousemove,mousedown
  • 百度搜索,搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流(throttle)

**所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。**节流会稀释函数的执行频率。

举个例子,乘坐地铁,过闸机时,每个人进入后3秒后门关闭,等待下一个人进入。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版

在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

function throttle(func, wait) {
    /***此处代码执行throttle函数就会执行,以下return的函数才是真正的事件触发函数,由此创建了一个全局变量 previous***/
    let previous = 0;
    return function() {
        const now = Date.now(); // 当前时间时间戳
        const args = arguments;
        if (now - previous > wait) {
            func.apply(this, args);
            previous = now;
        }
    }
}

定时器版

在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。

throttle(func, wait) {
    let timeout=null;
    return function() {
        const args = arguments;
        if (timeout===null) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(this, args);
            }, wait);
        }
    };
}

我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。

函数节流的应用场景

间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交
  • 轮播图