# 👉 防抖和节流

防抖和节流的主要作用都是为了减少函数无用的触发次数,以便解决响应跟不上触发频率导致页面卡顿这类问题,提高性能和避免资源浪费。

但防抖动和节流本质是不一样的,防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

# 防抖 debounce

# 防抖原理

在触发触发某个事件后的 n 秒后才能重新执行,若在触发事件的 n 秒内再次触发这个事件,时间 n 将会重新开始计时,直到触发完事件的 n 秒内不再触发事件,这个事件才会执行。(指定时间内只能触发一次事件,否则多次触发会重新计时。)

# 防抖应用场景

场景: 下载资源按钮,窗口 resize(只需要判断最后一次的变化情况))

用户在点击某个按钮后会触发某些任务事件,但若用户在很短时间内连续点击按钮,则会导致频繁触发任务事件。因此想要这个任务事件在一定时间内只触发一次(不管被点击了多少次),然后过了指定时间后点击才能重新被触发。

# 防抖实现

实现思路:设定一个定时器,满足设定等待的时间之后,执行函数并清空定时器,否则则不执行并重制定时器。

// 立即执行版防抖:表示不等事件停止触发后才执行,先立刻执行事件,等到停止触发 n 秒后,才可以重新触发执行。
// 非立即执行版防抖:表示需要等事件停止触发后的n秒后,才会执行。

/**
 * @desc 函数防抖
 * @param func 函数
 * @param timewait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
const _debounce = function(handler, timewait = 1000, immediate = true) {
    let timer, result;

    return function() {
        const args = arguments;
        const ctx = this;

        // 在timewait还没结束前,重新触发,就会重新开始计时
        if (timer) {
            // 这时timer还是等于定时器ID(clearTimeout并不会删掉timeout中保存的ID)
            clearTimeout(timer);
        }

        // 如果需要立即执行
        if (immediate) {
            // timer为空的时候才执行,即首次和timewait到期后
            // clearTimeout后,timer依然为一个定时器ID
            let callNow = !timer;

            timer = setTimeout(function() {
                timer = null;
            }, timewait);

            // 当且仅当到达 timewait 要求时,timer 才会被设置为 null,再触发 callNow 才会变成为 true
            if (callNow) {
                result = handler.apply(ctx, args);
            }
        } else {
            // 不需要立即执行
            timer = setTimeout(function() {
                timer = null;
                result = handler.apply(ctx, args);
            }, timewait);
        }

        return result;
    };
};
<div id="container"></div>

<script>
    let count = 1;
    const container = document.getElementById("container");

    const getActionText = function(e) {
        container.innerHTML = count++;
    };

    container.onmousemover = _debounce(getActionText, 1000);
</script>

# 节流 throttle

# 节流原理

当持续触发事件时,保证一定时间段内只能执行一次事件处理函数。(指定时间内多次触发时间后,需要间隔一定时间才会执行)

# 节流 应用场景

场景: 常用于输入框输入内容实时查询(间隔一段时间就必须查询相关内容,如果需要用户输入完再搜索可用防抖)、监听滚动条
原理: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

# 节流实现

和防抖类似,根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。

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

时间戳版实现原理:
获取每次执行的时间戳 previous,在再次触发时,判断当前时间戳和前一次时间戳差值,大于 timewait 则执行,否则不执行。

// 时间戳,首次事件会立刻执行一次,一直触发也会按照间隔执行,停止触发后不会再执行
function _throttle(handler, timewait = 2000) {
    let previous = 0,
        result;

    return function() {
        const ctx = this;
        const args = arguments;

        const now = +new Date();

        if (now - previous > timewait) {
            result = handler.apply(ctx, args);
            previous = now;
        }
        return result;
    };
}
// 定时器,事件会在 n 秒后第一次执行,停止触发后依然会再执行一次事件
const throttle = function(func, timewait) {
    let timer;

    return function() {
        const context = this;
        const args = arguments;

        // 指定时间内的触发都不会像防抖一样重置计时器,而是会丢到 setTimeout 的队列里等待执行并置空计时器
        if (!timer) {
            timer = setTimeout(function() {
                func.apply(context, args);
                timer = null;
            }, timewait);
        }
    };
};

结合前两种方案,我们再完善一个版本:
有头有尾的一个方案,首次需要一触发就立刻执行,结尾停止触发后还能执行最后一次

function _throttle(handler, timewait = 1000) {
    let timer, result;
    let previous = 0;

    return function() {
        const ctx = this;
        const args = arguments;

        const now = +new Date();
        const remain = timewait - (now - previous);

        // 首次会立刻执行,或者下一次触发时超过了timewait时间也可触发
        if (remain <= 0) {
            result = handler.apply(ctx, arguments);
            previous = now;

            // 若 timer 不为空,则置空清除,方便下次重新计时
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
        } else if (!timer) {
            // 如果还没超过timewait再次被触发,则会通过定时器缓存下来,倒计时执行
            timer = setTimeout(function() {
                result = handler.apply(ctx, arguments);
                timer = null;

                // 记录执行时间
                previous = +new Date();
            }, remain);
        }

        return result;
    };
}
<div id="container"></div>

<script>
    let count = 1;
    const container = document.getElementById("container");

    const getActionText = function(e) {
        container.innerHTML = count++;
    };

    container.onmousemover = _throttle(getActionText, 1000);
</script>