前端中的防抖和节流具体实现

哈喽 大家好!最近把面试总经常问到的防抖和节流知识整体总结一下,下面就分享给大家:

先介绍一下什么是防抖和节流,有两个问题可以有助于我们理解抽象的概念:为什么需要防抖和节流?哪些场景有都应用?

我们可以从这两个问题来介绍防抖和节流,从生活中举例子来理解这两个抽象概念

防抖执行的函数不会立即执行,例如:用户在输入框中频繁输入内容,搜索或者提交信息;频繁的点击某一个按钮,触发某个事件;监听浏览器的滚动事件,完成某些特定操作等等。

节流执行的函数会按照一定频率或者周期去执行,例如:用户频繁点击按钮操作、监听页面的滚动事件、鼠标移动事件等等。

防抖的原理是什么?

当某一个事件触发时,相应的函数不会立即触发,而是会等待一定时间,再去触发,例如:当用户在搜索框输入时需要请求请求服务器,如果每输入一个文字去请求服务器一定会降低服务器性能、增加服务器处理的压力,因为请求请求需要发送发给服务器很多次,只有等待了一段时间后用户没有再去触发事件,才会真正的执行响应函数;基于这个原理,我可以分步骤简单来实现一下防抖函数。(当然这里使用第三方库很多,例如:loadsh、underscore)

节流的原理又是什么?

当事件触发时,会执行这个事件的响应函数,如果这个事件会被频繁的触发,那么节流函数会按照一定的频率来执行,不管在这个中间有多少次触发事件,执行函数的频率总是固定不变的。(在某一段时间触发的事件是非常多的,但是我们只在某段时间只触发一次)

防抖函数的简单实现:

HTML代码:

<input type="text" class="text" />
<button class="btn">取消</button>

CSS代码:

input {
     width: 300px;
     height: 40px;
     line-height: 40px;
     font-size: 20px;
}
button {
     width: 100px;
      height: 45px;
     font-size: 20px;
}

JS代码:

/**
 * 没有参数的节流函数
 * @param {*} fn
 * @param {*} delay
 * @returns
 */
function debounce_A(fn, delay) {
    let timer = null;

    // 真正执行的函数
    const _debounce = function () {
        if (timer) {
            // 取消上一次的定时器
            clearTimeout(timer);
        }

        timer = setTimeout(function () {
            fn();
        }, delay);
    };

    return _debounce;
}

/**
 * 带有参数的节流函数
 * @param {*} fn
 * @param {*} delay
 * @returns
 */
function debounce_B(fn, delay) {
    let timer = null;

    // 真正执行的函数
    const _debounce = function (...args) {
        if (timer) {
            // 取消上一次的定时器
            clearTimeout(timer);
        }

        timer = setTimeout(() => {
            // 外部传入的真正要执行函数
            fn.apply(this, args);
        }, delay);
    };

    return _debounce;
}

/**
 * 第一次是否立即会执行节流函数 和 取消执行
 * @param {*} fn
 * @param {*} delay
 * @param {*} immediate
 */
function debounce_C(fn, delay, immediate = false) {
    let timer = null;
    let isInvoke = false;

    // 真正执行的函数
    const _debounce = function (...args) {
        if (timer) {
            // 取消上一次的定时器
            clearTimeout(timer);
        }

        if (immediate && !isInvoke) {
            // 第一次要执行的函数
            fn.apply(this, args);
            isInvoke = true;
        } else {
            timer = setTimeout(() => {
                // 外部传入的真正要执行函数
                fn.apply(this, args);
                // 间隔了一会儿,又想起来要输入,第一次需要被执行
                isInvoke = false;
            }, delay);
        }
    };

    // 取消功能
    _debounce.cancel = function () {
        if (timer) {
            clearTimeout(timer)
        }
        timer = null
        isInvoke = false
    }

    return _debounce;
}

分别对三个功能简单测试一下:

var btn = document.querySelector(".btn");
var text = document.querySelector(".text");
var count = 0;

const inputChange = function (event) {
     console.log(`第${++count}次网络请求`, this, event);
};

// text.oninput = debounce_A(inputChange, 2000);
// text.oninput = debounce_B(inputChange, 2000);
const debounceChange = debounce_C(inputChange, 2000, true);
text.oninput = debounceChange;

btn.addEventListener("click", function () {
     console.log(`已经取消第${++count}次网络请求`, this, event);
     debounceChange.cancel();
});

节流函数的简单实现:

/**
 * 基本实现
 * @param {*} fn
 * @param {*} interval
 * @returns
 */
function throttle_A(fn, interval) {
    // 记录上一次开始的时间
    let lastTime = 0;

    const _throttle = function () {
        // 2.获取当前函数触发的时间
        let nowTime = new Date().getTime();
        // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间
        let remainTime = interval - (nowTime - lastTime);

        if (remainTime <= 0) {
            fn();
            // 4.保留上次触发的时间
            lastTime = nowTime;
        }
    };

    return _throttle;
}
/**
 * 解决第一次执行函数后,如果再次触发就会立即执行,应该是第一次执行函数后,再次触发时,需要判断是否超过延时时间
 * leading :控制第一次要不要去执行的功能
 * @param {*} fn
 * @param {*} interval
 * @param {*} options
 * @returns
 */
function throttle_B(
    fn,
    interval,
    options = { leading: true, trailing: false }
) {
    // 记录上一次开始的时间
    let lastTime = 0;
    let timer = null;
    let { leading, trailing } = options;

    const _throttle = function () {
        // 2.获取当前函数触发的时间
        let nowTime = new Date().getTime();
        // 2.1 如果是第一次执行才会去触发, 把第一次/上一次执行的执行时间记录
        if (!lastTime && !leading) {
            lastTime = nowTime;
        }

        // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间
        let remainTime = interval - (nowTime - lastTime);

        if (remainTime <= 0) {
            fn();
            // 4.保留上次触发的时间
            lastTime = nowTime;
        }
    };

    return _throttle;
}
/**
 * 是否延迟执行
 * @param {*} fn
 * @param {*} interval 是否需要立即执行
 * @param {*} options
 * @returns
 */
function throttle_C(
    fn,
    interval,
    options = { leading: true, trailing: false }
) {
    // 记录上一次开始的时间
    let lastTime = 0;
    let timer = null;
    //  leading:开始是否触发 , trailing:最后一次是否会执行
    let { leading, trailing } = options;

    const _throttle = function (...args) {
        // 2.获取当前函数触发的时间
        let nowTime = new Date().getTime();
        // 2.1 如果是第一次执行才会去触发, 把第一次/上一次执行的执行时间记录
        if (!lastTime && !leading) {
            lastTime = nowTime;
        }

        // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间
        let remainTime = interval - (nowTime - lastTime);
        if (remainTime <= 0) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }

            // 3.1 真正执行触发的函数
            fn.apply(this, args);
            // 4.保留上次触发的时间
            lastTime = nowTime;
            return;
        }

        // 5. 判断最后一次是否会执行
        if (trailing && !timer) {
            timer = setTimeout(() => {
                timer = null;
                // 控制最后一次执行的时间
                lastTime = leading ? new Date().getTime() : 0;
                fn.apply(this, args);
            }, remainTime);
        }
    };

    return _throttle;
}

 

posted @ 2022-06-05 18:25  宇宙星空  阅读(963)  评论(0编辑  收藏  举报