# 👉 防抖和节流

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

但防抖动和节流本质是不一样的,核心区别在于触发时机和执行频率的控制:防抖是多次触发但只执行最后一次;节流是将多次执行变成每隔一段时间执行。

# 防抖 debounce

# 防抖原理

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

# 防抖应用场景

  • 搜索框输入联想(用户停止输入后再请求)
  • 窗口大小调整 resize(调整结束后判断最后一次的变化情况)
  • 表单提交防重复点击(最后一次点击生效)

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

# 防抖实现

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

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

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

    return function(...args) {
        const ctx = this;
        let result;

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

        // 如果需要立即执行
        if (immediate) {
            // timer为空的时候才执行,即首次和delay到期后
           // 当且仅当到达 delay 要求时,timer 才会被设置为 null,再触发 callNow 才会变成为 true
            let callNow = !timerId;
            if (callNow) {
                result = handler.apply(ctx, args);
            }

            timerId = setTimeout(function() {
                timerId = null;
            }, delay);

        } else {

            // 不需要立即执行
            timerId = setTimeout(function() {
                timerId = null;
                result = handler.apply(ctx, args);
            }, delay);
        }

        return result;
    };
};
<button id="debounce-btn">
    <span class="btn">防抖: </span>
    <span class="count" id="debounce-count">0</span>
</button>

<script>
    const getDomById = (id) => document.getElementById(id);

    const addCount = (countDomId) => {
        const countDom = getDomById(countDomId);
        let countVal = Number(countDom.innerText || 0);
        countVal += 1;
        countDom.innerText = countVal;
    }

    const debounceBtn = getDomById('debounce-btn');
    const debounceOnClick = _debounce(addCount, 1000, false);
    debounceBtn.addEventListener('click', () => {
        debounceOnClick('debounce-count');
    })

</script>

# 节流 throttle

# 节流原理

当持续触发事件时,保证固定时间间隔内只能执行一次事件回调。 (指定时间内多次触发时间后,需要间隔一定时间才会执行。比如滚动事件,不管用户滚动多快,每200ms只处理一次。)

# 节流应用场景

  • 输入框输入,内容实时查询(间隔一段时间就必须查询相关内容,如果需要用户输入完再搜索可用防抖)
  • 监听滚动条,页面滚动加载更多(间隔检查滚动位置)
  • 鼠标移动事件(如拖拽元素时减少计算频率,如果用消抖会卡顿)

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

原理: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

# 节流实现

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

对于节流,一般有两种方式可以实现:

  1. 时间戳版:时间戳的方式会在事件开始时触发
  2. 定时器版:定时器的方式会在事件结束后触发

结合两者可以确保在开始和结束时都触发一次,比如在鼠标移动时,既立即响应第一次,又在停止后触发最后一次。

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

    // 时间戳,首次事件会立刻执行一次,一直触发也会按照间隔执行,停止触发后不会再执行
    function _throttle(handler, delay = 2000) {
        let previous = 0,
    
        return function(...args) {
            const ctx = this;
            const now = +new Date();
            let result;
    
            if (now - previous > delay) {
                result = handler.apply(ctx, args);
                previous = now;
            }
    
            return result;
        };
    }
    
  2. 定时器版本

    // 定时器,事件会在 n 秒后第一次执行,停止触发后依然会再执行一次事件
    const throttle = function(func, delay) {
        let timer;
    
        return function(...args) {
            const context = this;
            let result;
    
            // 指定时间内的触发都不会像防抖一样重置计时器,而是会丢到 setTimeout 的队列里等待执行并置空计时器
            if (!timer) {
                timer = setTimeout(function() {
                    result = func.apply(context, args);
                    timer = null;
                }, delay);
            }
    
            return result;
        };
    };
    
  3. 结合前两种方案,我们再完善一个版本:
    有头有尾的一个方案,首次需要一触发就立刻执行,结尾停止触发后还能执行最后一次

    /**
    * @desc 函数截流
    * @param handler 函数
    * @param delay 延迟执行毫秒数
    * @param options {leading, trailing} leading 首次是否执行, trailing 结束是否再执行一次
    */
    function _throttle(handler, delay = 1000, options = {leading: true,trailing: false}) {
        let timer = null;
        let previous = 0;
        const { leading, trailing } = options;
    
        return function(...args) {
            let result;
            const ctx = this;
    
            // 计算剩余时间
            const now = +new Date();
            const remain = delay - (now - previous);
    
            // 1. 首次调用立刻执行,或者下一次触发时超过了delay时间也可触发
            if (remain <= 0 && leading) {
                result = handler.apply(ctx, arguments);
                previous = now;
    
                // 若 timer 不为空,则置空清除,方便下次重新计时
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
            } else if (!timer && trailing) {
                // 如果还没超过delay再次被触发,则会通过定时器缓存下来,倒计时执行
                timer = setTimeout(function() {
                    result = handler.apply(ctx, arguments);
    
                    // 记录执行时间
                    previous = +new Date();
    
                    // 重置计时器
                    timer = null;
                }, remain);
            }
    
            return result;
        };
    }
    
    <div id="throttle">
        <span>节流:</span>
        <input id="throttle-input"></input>
        <div>
        <span id="throttle-output"></span>
        </div>
    </div>
    
    <script>
        const THROTTLE_INPUT = 'throttle-input';
        const THROTTLE_OUTPUT = 'throttle-output';
    
        const throttleInput = getDomById(THROTTLE_INPUT);
    
        const inputToOutput = (inputId, outputId) => {
        const throttleInput = getDomById(inputId);
        const throttleOutput = getDomById(outputId);
    
        const inputText = throttleInput.value;
        throttleOutput.innerText = inputText;
        };
    
        const inputToOutputFn = _throttle(inputToOutput, 1000, {
        trailing: true,
        leading: true,
        });
    
        throttleInput.addEventListener('input', () => {
        inputToOutputFn(THROTTLE_INPUT, THROTTLE_OUTPUT);
        });
    </script>