[ 原生 JS 造轮子 ] - 一:防抖节流

751 阅读4分钟

说在前面的

防抖和节流都是前端日常开发中经常用到的技巧,那么什么是防抖和节流呢?这里线简单介绍一个场景:

假设当前有一个表单页,用户填好表单之后点击按钮提交表单,代码如下:

<button id="submit-btn">Submit</button>
function submit() {
    console.log("submit");
}

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", submit);

如果这个时候,如果用户多次点击,效果如下:

img-00

很明显提交的请求就会多次被发送到后端,占用资源,这显然是我们不想见到的。

而防抖,节流就是为了应付这样的场景出现的,只是各自效果有一些细微的差别罢了。

防抖

我们首先来看看防抖的效果:

img-01

可以看到,当我们点击按钮的时候,并不会立即触发效果,而是在一定的延迟之后才触发效果,并且如果不停的点击按钮,只有在最后一次点击结束之后再经过一定的延迟,才会触发效果。

这就是防抖的功能了:

  • 如果被防抖修饰的方法在一定时间内被不断的触发,只有最后一次触发会生效

知道了功能,我们就能根据功能来实现函数了。

首先,我们需要一个防抖函数去修饰提交的方法,所以在点击按钮的时候,我们调用的应该是这个防抖函数:

function submit() {
    console.log("submit");
}

const debounce = (fn) =>{
    fn()
}

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", debounce(submit));

这个时候,我们点击按钮,效果如下:

img-03

可以看到,屏幕都戳烂了也没有触发 submit 的效果,原因是 debounce 虽然调用了 submit 函数,却没有返回,所以这里直接返回一个函数,在函数中调用 fn,顺便再小小优化一下代码:

function submit() {
    console.log("submit");
}

const debounce = (fn) =>{
    return () => {
        fn();
    };
}

const oDebounce = debounce(submit);

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", oDebounce);

效果如下:

img-04

到这里又回到了最初的状态,不过与一开始的区别是,我们已经可以通过 debounce 来修饰 submit 了,那么再来看看防抖的核心功能

  • 如果被防抖修饰的方法在一定时间内被不断的触发,只有最后一次触发会生效

既然要让函数延迟,肯定免不了 setTimeout,那么我们将 fn 放到 setTimeout 中,再加上延迟:

function submit() {
    console.log("submit");
}

const debounce = (fn, delay) =>{
    return () => {
        setTimeout(() => {
            fn();
        }, delay);
    };
}

const oDebounce = debounce(submit, 1000);

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", oDebounce);

效果如下:

img-05

可以看到,当连续点击的时候,fn 函数在延迟 1s 之后都一块儿执行了,这就要谈谈 setTimeout 的运行机制了:

setTimeout 的运行机制是,将指定的代码移出本次执行,等到下一 轮Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。

这就很好的解释了,为什么连续触发 setTimeout 的时候,在延迟之后,函数都一块儿触发了。

防抖的需求是,只响应最后一次点击,那么我们可以通过清除定时器来完成这个效果:

function submit() {
    console.log("submit");
}

const debounce = (fn, delay) =>{
    let timer;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn();
        }, delay);
    };
}

const oDebounce = debounce(submit, 1000);

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", oDebounce);

效果如下:

img-06

到这里已经实现了防抖的基本功能了,当然,我们可以继续优化一下

  • 绑定 this 指向
  • fn 传参

考虑以上两点,最终代码如下:

function submit() {
    console.log("submit");
}

const debounce = (fn, delay) =>{
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

const oDebounce = debounce(submit, 1000);

const btn = document.getElementById("submit-btn");
btn.addEventListener("click", oDebounce);

节流

节流与防抖基本是类似的,同样的,我们先来看看节流的效果:

img-07

可以看出,节流的效果是:

  • 在一定时间之内,被节流函数修饰的函数只会触发一次

结合这个功能,很容易就能想到下面的思路:

  • setp1:我们设置一个开关,当节流函数启动的时候关掉这个开关
  • step2:当开关关闭的时候对外界不做任何响应
  • step3:当节流函数执行完毕之后重新打开开关

有了这个思路,节流函数的实现也就差不多了(这里顺带把优化也做了):

const throttle = (fn, delay) => {
    let tSwitch = true;
    return (...args) => {
        if (!tSwitch) return;
        tSwitch = false;
        setTimeout(() => {
            fn.apply(this, args);
            tSwitch = true;
        }, delay);
    };
};

结束语

这里是 在线 demo