前言

函数的防抖与节流在项目中有很多的使用场景,本文参考了冴羽的两篇文章。

函数防抖

概念

指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

举个栗子,坐电梯的时候,如果电梯检测到有人进来(触发事件),就会多等待 10 秒,此时如果又有人进来(10秒之内重复触发事件),那么电梯就会再多等待 10 秒。在上述例子中,电梯在检测到有人进入 10 秒钟之后,才会关闭电梯门开始运行,因此“函数防抖”的关键在于,在一个事件发生一定时间之后,才执行特定动作。

场景代码 html

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
  <title>Document</title>
  <style>
    #container {
      width: 100%;
      height: 200px;
      line-height: 200px;
      text-align: center;
      color: #fff;
      background-color: #444;
      font-size: 30px;
    }
  </style>
</head>
<body>
    <div id="container"></div>
    <script>
        var count = 1;
        var container = document.getElementById('container');

        function getUserAction() {
            container.innerHTML = count++;
        };

        container.onmousemove = debounce(getUserAction, 1000);

        function debounce () {
            ...
        }
    </script>
</body>
</html>

基础版

根据上面的表述,实现第一版的代码:

function debounce(func, wait) {
    var timeout;

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

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

第一版的代码没什么难点。一个事件在 n 秒内重复触发,会一直清除定时器,等停止触发 n 秒后定时器的方法才会执行。同时也解决了定时器中方法的 this 指向和 JavaScript 在事件处理函数中会提供事件对象 event 参数问题。

立刻执行

第一版代码事件需要在 n 秒后执行。一个新的需求是希望立刻执行函数,然后等到停止触发 n 秒后,再重新触发执行。

增加 immediate 参数判断是否是立刻执行。

function debounce(func, wait, immediate) {
    var timeout;

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

        if (timeout) clearTimeout(timeout);

        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function () {
                timeout = null
            }, wait);
            
            if (callNow) func.apply(this, args) 
        } else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

返回值

getUserAction 函数可能是有返回值的,所以这里也需要返回函数的结果。但当 immediate 为 false 的时候,因为使用 setTimeout,在最后 return 的时候值会一直是 undefined。所以只在 immediate 为 true 的时候返回函数的执行结果。

function debounce(func, wait, immediate) {
    var timeout, result;

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

        if (timeout) clearTimeout(timeout);

        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function () {
                timeout = null
            }, wait);
            
            if (callNow) result = func.apply(this, args) 
        } else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
        return result
    }
}

取消

实现让用户执行 cancel 方法来取消防抖,当用户再次去触发时又可以立刻执行。

function debounce(func, wait, immediate) {
    var timeout, result;

    var debounced = function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);

        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function () {
                timeout = null
            }, wait);
            
            if (callNow) result = func.apply(this, args) 
        } else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
        return result
    }

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced
}

函数节流

概念:限制一个函数在一定时间内只能执行一次。

举个栗子,坐火车或地铁,过安检的时候,在一定时间(例如10秒)内,只允许一个乘客通过安检入口,以配合安检人员完成安检工作。上例中,每10秒内,仅允许一位乘客通过。分析可知“函数节流”的要点在于,在一定时间之内,限制一个动作只执行一次 。

// func (Function): 要节流的函数
// [wait=0] (number): 需要节流的毫秒
// [options={}] (Object): 选项对象
// [options.leading=true] (boolean): 指定调用在节流开始前
// [options.trailing=true] (boolean): 指定调用在节流结束后
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function () {
        previous = options.leading === false ? 0 : Date.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now();
        if (!previous && options.leading === false) previous = now;

        var remaining = wait - (now - previous);
        context = this;
        args = arguments;

        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
}

参考文章