【面试】手写防抖与节流

94 阅读3分钟

题目一 手写防抖 + 立即执行

一段时间内连续触发事件,在最后一次触发事件后wait事件后再触发事件。

应用场景:

  1. 按钮防重
  2. 搜索框sug,最后输入完再请求
  3. input输入框,输入完再校验格式
function debounce(func, wait, immediate) {
    let timer;
    let result;
    var debounced = function () {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
        }, wait);
        return result;
    };
    return debounced;
}

如果支持一个功能:一触发立即执行,增加参数immediate

  • 判断如果timer为空,则执行
  • 然后启动timer,wait时间后 timer再次设置为空,后续再触发可以执行
  • wait期间如果想取消,不想等那么久,则加一个cancel方法,将timer重制为空
/**
 */
function debounce(func, wait, immediate) {
    let timer;
    let result;
    var debounced = function () {
        if (timer) {
            clearTimeout(timer);
        }
        if (immediate) {
            if (!timer) {
                result = func.apply(this, arguments)
            }
            // timer赋值,这段时间内触发不会再次触发,wait后可以再次触发
            timer = setTimeout(function(){
                timer = null;
            }, wait)
        } else {
            timer = setTimeout(() => {
                func.apply(this, arguments);
            }, wait);
        }
        return result;
    };
    // 取消防抖,这样再次点击就可以直接执行
    debounced.cancel = function() {
        clearTimeout(timer);
        timer = null
    }
    return debounced;
}

var testDebounce = debounce(() => console.log('scroll'), 1000);
window.onscroll = testDebounce

题目二 手写节流 + 有头有尾

持续触发事件,每隔一段时间,只执行一次事件。

有两种主流的实现方式:

  • 使用时间戳,
  • 设置定时器。

两种区别是:

  • 使用时间戳是立即执行一次,事件停止后不再触发,
  • 使用定时器则刚开始不执行,wait后执行一次,最后事件停止wait后再触发一次

应用场景:

  1. 页面滚动,监听页面滑动到底部
  2. sug搜索
// 时间戳方式:会立即执行一次,没有触发后不执行
function throttle(func, wait) {
    let last = 0;
    return function () {
        const now = +new Date(); // 转时间戳
        if (now - last > wait) {
            func.apply(this, arguments);
            last = now;
        }
    }
}

// 定时器:刚开始不执行,wait后执行一次,最后事件停止触发wait后还会触发一次,
// 开启一个定时器,如果timer有值,直接返回,到wait执行后再将timer置空,重新开启一个定时器,
// 相当于定时器模拟了setInterval效果
function throttle(func, wait) {
    let timer;
    return function () {
        if (timer) {
            return;
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
            timer = null;
        }, wait);
    }
}

如果增加一个功能,想开头也触发,结尾也触发,怎么办?那就需要将上面两种情况结合一起写。不能直接用标志位,用标志位就只触发一次了,后面再触发都无效了。

// 想开头触发,也想结束触发
function throttle(func, wait) {
    let last = 0;
    let timer;
    return function () {
        const now = +new Date();
        // 下次触发剩余的时间,如果小于0定时器方式触发,如果大于0并且timer为空,则初始化timer,每次触发后last重置,timer也清空
        const remain = wait - (now - last);
        // 第一次都使用计时方式
        if (remain <= 0) {
            last = now;
            if (timer) {
                clearTimeout(timer);
                timer = null
            }
            return func.apply(this, arguments);
        } else if (!timer) { // 中间大部分这里,最后一次是使用timer
            timer = setTimeout(() => {
                func.apply(this, arguments);
                last = +new Date(); // 更新执行时间
                timer = null;
            }, remain);
        }
    };
}

var testThrottle = throttle(() => console.log('throttle scroll'), 1000);
window.onscroll = testThrottle

如果我们传入一个类似库函数的参数options,有两个值, 可以通过参数形式配置出来无头有尾,有头无尾,有头有尾。

options = {
    leading:false, // 表示禁用第一次执行
    trailing: false // 表示禁用停止触发的回调, 则会使用计时器方式触发,如果为true则使用timer的方式
}
function throttle(func, wait, options) {
    let last = 0;
    let timer;
    return function () {
        const now = +new Date();
        // 禁用第一次执行
        if (!last && options.leading === false) {
            last = now;
        }
        // 下次触发剩余的时间
        const remain = wait - (now - last);
        // 如果开头触发,会走到这里
        if (remain <= 0) {
            last = now;
            if (timer) {
                clearTimeout(timer);
                timer = null
            }
            return func.apply(this, arguments);
        } else if (!timer && options.trailing) { // 禁用最后一次,那都使用第一种方法执行
            timer = setTimeout(() => {
                func.apply(this, arguments);
                last = +new Date();
                timer = null;
            }, remain);
        }
    };
}

// 或者主要是使用计时器,最后一次使用timer,相等于和debounce结合了
function throttle2(fn, delay, options) {
    let last = 0;
    let timer = null;
    return function () {
        const now = new Date();
        // 禁用第一次执行
        if (!options.leading && !last) {
            last = now;
            return;
        }
        if (now - last > delay) {
            fn(...arguments);
            last = now;
        } else if (options.trailing) {
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn(...arguments)
            }, now - last);
        }
    }
}