工具函数系列-节流throttle

节流的原理是:固定时间间隔执行函数

应用场景:resizescroll

function throttle(func, wait) {
    var timeout, context, args;
    var previous = 0;

    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };

    var throttled = function() {
        var now = +new Date();
        //下次触发 func 剩余的时间
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

function throttle2(func, wait) {
    var timeout = null;
    var previous = 0;

    return function() {
        var args = arguments;
        var context = this;
        var now = +new Date(); 
        var remainning = wait - (now - previous); // 等待时间减去两次触发事件的时间差
        timeout && clearTimeout(timeout)
        if (remainning <= 0) { // 两次触发时间差大于等待时间,立马执行
            func.apply(context, args)
            previous = now // 更新时间
        } else { // 小于等待时间,等待一段时间后执行
            timeout = setTimeout(function () {
                func.apply(context, args)
                provious = +new Date()
            }, remainning) // 修改设置timeout时间
        }
    }
}

我们可以看到:鼠标移入,事件立刻执行,晃了 3s,事件再一次执行,当数字变成 3 的时候,也就是 6s 后,我们立刻移出鼠标,停止触发事件,9s 的时候,依然会再执行一次事件。

优化

取消第一次执行或者取消最后一次执行

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

leading和trailing不能同时设置,如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了

function throttle(func, wait, options) {
    var timeout, context, args;
    var previous = 0;
    if (!options) options = {}; // 设置默认值 可以使用es6设置默认值的方式
    
    var later = function() {
        previous = options.leading === false ? 0 : +new Date();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };
    
    var throttled = function() {
        var now = +new Date();
        if (!previous && options.leading === false) previous = now;
        var remainning = wait - (now - previous);
        context = this;
        args = arguments;
        if (remainning <= 0 || remainning > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remainning)
        }
    }
    return throttled
}
取消
// 接上一版
throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}
// ...

参考文章:https://github.com/mqyqingfeng/Blog/issues/26

测试
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</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() {
            let num = count++
            container.innerHTML = num;
            console.log(Date.now(), num)
        };

        container.onmousemove = throttle(getUserAction, 1000);

        function throttle(func, wait) {
            var timeout, context, args, result;
            var previous = 0;

            var later = function() {
                previous = +new Date();
                timeout = null;
                func.apply(context, args)
            };

            var throttled = function() {
                var now = +new Date();
                //下次触发 func 剩余的时间
                var remaining = wait - (now - previous);
                context = this;
                args = arguments;
                // 如果没有剩余的时间了或者你改了系统时间
                if (remaining <= 0 || remaining > wait) {
                    if (timeout) {
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    previous = now;
                    func.apply(context, args);
                } else if (!timeout) {
                    timeout = setTimeout(later, remaining);
                }
            };
            return throttled;
        }
    </script>
</body>

</html>