JS函数防抖和节流

在前端开发的过程中,我们经常会遇到连续触发的事件,比如resize、scroll、keydown 等等,有些时候,比如触发事件会有Ajax请求,耗时的运算,页面渲染等,我们不希望在事件持续触发的过程中那么频繁地去执行函数。
一般来说,防抖和节流是比较好的解决方案。

tips: 防抖和节流都是利用了闭包来保持对函数内变量的持续引用
另外还需要注意的是 this 和 参数的传递

防抖(debounce)

防抖就是在事件触发n毫秒后执行函数,如果在这期间再次触发则重新计算函数执行事件,即:连续的触发中只有最后一次才会执行函数

function debounce (func, wait) {
    let timeout;
    return function () {
        /* 
            注意: setTimeout 使用了箭头函数,不需要 
            const context = this; 
            this指向当前上下文
        */
        const args = arguments;
        // 每次触发事件,都清除上次的计时器,生成新的计时器重新计算函数执行时间
        if (timeout) clearTimeout(timeout); 
        timeout = setTimeout(() => {
            /* 
                注意:apply 手动绑定this
                如果不这样做,func中的this会指向函数声明位置的上下文
            */
            func.apply(this, args);
        }, wait);
    };
}

立即执行版:

    // leading = true 立即执行
function debounce (func, wait, leading) {
    let timeout;
    return function () {
        const args = arguments;
        if (timeout) clearTimeout(timeout);
        if (leading) {
            if (!timeout) func.apply(this, args);
            timeout = setTimeout(() => {
                func.apply(this, args);
                timeout = null;
            }, wait);
        } else {
            timeout = setTimeout(() => {
                func.apply(this, args);
            }, wait);
        }

    };
}

节流(throttle)

节流是指连续触发的事件n毫秒内只执行1次,节流会稀释函数的执行频率
区别于防抖,防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行

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

时间戳版:

function throttle (func, wait) {
    let prev = 0;
    return function () {
        const now = new Date();
        const args = arguments;
        if (now - prev > wait) {
            func.apply(this, args);
            prev = now;
        }
    };
}

定时器版:

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

我们可以看到时间戳版和定时器版的差别是:定时器第一次触发不会立刻执行,在一段时间的最后执行一次,有一个延迟的效果。时间戳版的是第一次立即执行,结尾没有执行。

单独用时间戳版和定时器版都有缺陷,我们更希望第一次触发马上执行函数,结尾也执行一次。且连续触发时,是每隔n毫秒触发一次的均匀的执行。

我们将间戳版和定时器结合一下,得到下面的试验版

function throttle(func, wait) {
    let timeout;
    let prev = 0;
    return function () {
        const args = arguments;
        const now = new Date();
        if ( now - prev > wait) {
            func.apply(this, args);
            prev = now
        } else {
            if(!timeout) {
                timeout = setTimeout(() => {
                    func.apply(this, args);
                    timeout = null;
                    prev = new Date() // 注意这里不能用prev = now 这是个闭包
                }, wait)
            }
        }
    }
}

很容易看出来,这样没有办法保证连续触发的情况下函数执行时间是连续的间隔n,应该给定制器设置一个剩余执行时间 remaining

function throttle (func, wait) {
    let timeout;
    let prev = 0;
    return function () {
        const args = arguments;
        const now = new Date();
        const remaining = wait - (now - prev); //剩余时间
        /* 
            注意:不能写成 remaining <= 0 
            因为当remaining=0时恰好触发,会触发立即执行,
            此时上次设置的定时器也刚好到了执行时间(假设setTimeout没有延迟的情况下),就会有2次连续执行
        */
        if (remaining < 0) { // 第一次触发立即执行
            func.apply(this, args);
            prev = now;
        } else {
            if (!timeout) {
                timeout = setTimeout(() => {
                    func.apply(this, args);
                    timeout = null;
                    prev = new Date();
                }, remaining);
            }
        }
    };
}

这样就完美实现了,如果想用开关开控制

/*
func (Function): 要节流的函数。
[wait=0] (number): 需要节流的毫秒。
[options={}] (Object): 选项对象。
[options.leading=true] (boolean): 指定调用在节流开始前。
[options.trailing=true] (boolean): 指定调用在节流结束后。
*/
function throttle (func, wait, { leading = true, trailing = true } = {}) {
    let timeout;
    let prev = 0;
    return function () {
        const now = new Date();
        const remaining = wait - (now - prev);
        const args = arguments;
        if (remaining < 0 && leading) {
            func.apply(this, args);
            prev = now;
        } else if (!timeout && trailing) {
            timeout = setTimeout(() => {
                func.apply(this, args);
                timeout = null;
                prev = new Date();
            }, trailing ? (leading ? remaining : wait) : remaining);
        }
    };
}

有错误欢迎指出~

你可能感兴趣的:(javaScript)