防抖和节流

防抖和节流

防抖和节流在前端日常开发中使用的的频率相当高,在小程序还有Hybrid应用开发中也会使用到它。像underscore中就提供了这样的高阶函数。

防抖常见的使用场景有:

  • 监听scroll的滚动
  • 搜索框输入查询
  • 表单验证
  • 按钮点击事件
  • 浏览器窗口缩放(resize事件)

防抖(debounce)

原理:事件响应函数在第一次被触发时,并不会立即执行,而是在一个计时器阈值后执行。例如我们设置的计时器的阈值是300ms,如果在这300ms内没有再次触发这个事件响应函数,那么就执行这个响应函数;如果在300ms内再次触发这个事件响应函数,那么当前的计时取消,重新开始计时。

这样,我们如果在300ms时间内大量触发同一个响应事件,最终只会执行一次函数。

使用underscore的高阶函数来实现防抖:

//使用underscore的高阶函数来实现防抖
container.onmousemove = _.debounce(mouseMove, 300);

//设置为true,则会立即触发一次函数执行,默认为false
container.onmousemove = _.debounce(mouseMove, 300, true);

我们可以模仿underscore中的实现,将防抖抽取成一个debounce.js文件,里面这样实现即可:

function debounce(func, wait, immediate) {
    var timeout, result;
    var debounced = function() {
        //改变内部函数的this的指向
        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(context,args);
        } else {
            timeout = setTimeout(function() {
                func.apply(context,args);
            }, wait)
        }
        return result;
    }
    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    }
    return debounced;
}

下面,我们演示如何使用:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #container {
            width: 100%;
            height: 400px;
            line-height: 400px;
            text-align: center;
            background-color: #111;
            color: #fff;
            font-size: 40px;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <button id="button">取消防抖</button>
    <script src="https://cdnjs.gtimg.com/cdnjs/libs/underscore.js/1.7.0/underscore.js"></script>
    <script src="./debounce.js"></script>
    <script>
        let count = 0;
        let container = document.getElementById('container');
        let button = document.getElementById('button');
        function mouseMove(event) {
            container.innerHTML = count++;
            console.log(this);
            console.log(event);
            return '哈哈哈哈哈'
        }
        // 会多次触发鼠标移动事件
        // container.onmousemove = mouseMove;
        //使用underscore的高阶函数来实现防抖
        // container.onmousemove = _.debounce(mouseMove, 300);
        //设置为true,则会立即触发一次函数执行,默认为false
        //  container.onmousemove = _.debounce(mouseMove, 300, true);


         let debounced = debounce(mouseMove, 3000);
         container.onmousemove = debounced;
         button.onclick = function() {
             console.log('取消防抖');
            debounced.cancel();
         }
    </script>
</body>
</html>

我们创建一个html,然后引用我们的debounce文件。对比使用underscore框架,效果一致。

节流(throttle)

原理:如果你持续触发一个响应事件,那么每隔一段时间,就会执行这个事件的回调函数。

我们可以从使用场景来区别防抖和节流。假如我们有一个很长的列表,用户在不间断(触发间隔小于300ms)的滚动。我们如果使用防抖(设置超时阈值为300ms),那么我们的事件回调只有等用户停止滚动(间隔大于300ms)后才会执行,如果用户总是在不间断的滚动,那么理论上我们的事件回调就不会执行;现在,我们希望用户在间断的滚动过程中,每隔(2000ms)一段时间,就显示当前滚动距顶部的距离,那么就需要使用节流了。

节流常见的使用场景有:

  • DOM元素的拖拽
  • 计算鼠标的移动距离
  • 监听scroll的滚动
  • 搜索框输入查询

使用underscore的高阶函数来实现防抖:

 container.onmousemove = _.throttle(mouseMove, 2000,{
             leading: false,
             trailing: false
         });

默认情况下,在underscore中,leading默认为true,表示当你触发事件时会立即执行响应回调函数;trailing默认为true,表示当你停止触发事件(此时正处于下一次2000ms的回调间隔内),会执行这最后一次的回调。需要注意的是,leading和trailing不能同时为false,否则会有bug。现象是“第一次”可以达到我们想要的同时为false的效果,“第二次”则不能达到同时为false的效果。

节流可以有2种方式实现,一种是使用时间戳,一种是定时器,下面我们将分别实现它。

使用时间戳的方式实现节流

// 相当于underscore中leading: true ,trailing: false的情况
function throttle(func, wait) {
    var context, args;
    var old = 0;
    return function() {
        context = this;
        args = arguments;
        var now = new Date().valueOf();
        if (now - old > wait) {
            old = now;
            func.apply(context,args);
        }
    }
}

使用定时器的方式实现节流

// 相当于underscore中leading: false ,trailing: true的情况
function throttle(func, wait) {
    var context, args, timeout;
    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function() {
                timeout = null;
                func.apply(context,args);
            }, wait);
        }
    }
}

结合定时器和时间戳来实现节流

// 相当于underscore中leading: true ,trailing: true的情况
function throttle(func, wait) {
    var context, args, timeout;
    var old = 0;
    return function() {
        context = this;
        args = arguments;
        var now = new Date().valueOf();
        if (now - old > wait) {
            old = now;
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context,args);
        } else if (!timeout) {
            timeout = setTimeout(function() {
                timeout = null;
                old = new Date().valueOf()
                func.apply(context,args);
            }, wait);
        }
    }
}

仿照underscore框架实现最终版

模仿underscore,可以通过options来控制第一次是否执行和最后一次是否执行的情况:

function throttle(func, wait,options) {
    if (!options) options = {};
    var context, args, timeout;
    var old = 0;
    return function() {
        context = this;
        args = arguments;
        var now = new Date().valueOf();
        if (!old && options.leading === false) {
            old = now;
        }
        if (now - old > wait) {
             // 第一次会执行
            old = now;
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context,args);
        } else if (!timeout && options.trailing !== false) { 
            // 最后一次会执行
            timeout = setTimeout(function() {
                timeout = null;
                old = new Date().valueOf()
                func.apply(context,args);
            }, wait);
        }
    }
}

下面,我们演示如何使用:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #container {
            width: 100%;
            height: 400px;
            line-height: 400px;
            text-align: center;
            background-color: #111;
            color: #fff;
            font-size: 40px;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <script src="https://cdnjs.gtimg.com/cdnjs/libs/underscore.js/1.7.0/underscore.js"></script>
    <script src="./throttle.js"></script>
    <script>
        let count = 0;
        let container = document.getElementById('container');
        let button = document.getElementById('button');
        function mouseMove(event) {
            container.innerHTML = count++;
            console.log(this);
            // console.log(event);
            // return '哈哈哈哈哈'
        }    
        //使用underscore的高阶函数来实现节流
        //  container.onmousemove = _.throttle(mouseMove, 2000,{
        //      leading: true,
        //      trailing: false
        //  });

        container.onmousemove = throttle(mouseMove, 2000, {
             leading: false,
             trailing: true 
        });
    </script>
</body>
</html>

总结

防抖和节流应该算是对性能的优化。至于何时使用,还要结合需求场景。

我们注意到,对于搜索框输入查询,我们即可以使用防抖方案,也可以使用节流方案。但是,二者的业务需求是不同的。例如如果要支持输入“实时搜索”,那么使用节流方案就比较合适;如果,我们只是希望用户在超过一段时间后当成“输入完成”才去搜索,那么使用防抖方案是比较合适的。


   转载规则


《防抖和节流》 刘星星 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Charles的使用与配置 Charles的使用与配置
基本使用Charles是使用简单,也是开发过程中非常有用的工具,这里简单介绍下使用。 对于开发iOS的同学来说,如果要使用模拟器进行调试,配置非常简单。 第一步,打开Charles,勾选macOS Proxy: 第二步,再在hel
下一篇 
微信小程序蓝牙热敏打印机调试总结 微信小程序蓝牙热敏打印机调试总结
### 1.扫描设备筛选 要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉
2020-02-21
  目录