19.函数的防抖

函数的防抖

介绍

函数防抖中的抖动就是执行的意思,而一般的抖动都是持续的、多次的、频繁的执行某一段代码。函数防抖就是某函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。在前端开发中经常会遇到这种频繁的事件触发,比如:

  • window 的 resize、scroll
  • mousedown、mousemove
  • keyup、keydown 、等等…

观察以下示例代码来了解事件如何频繁的触发:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<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() {
        container.innerHTML = count++;
    };
    // 鼠标移动时触发指定函数getUserAction
    container.onmousemove = getUserAction;
    </script>
</body>

</html>

上面的案例效果如图所示:
无防抖效果

解释:上面简单例子中鼠标从左边滑到右边就触发了 165 次 getUserAction 函数,若是复杂的回调函数或是网络请求请求,在 1 秒触发了 60 次,那么每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。严重的影响了用户体验与应用的性能。为了解决这个问题,一般有两种解决方案:

  1. 函数的防抖(debounce)
  2. 函数的节流(throttle)

概念

函数防抖和节流,都是控制事件触发频率的方法。其中防抖的原理就是:事件尽管触发,但是在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发相同事件,那我就以新的事件 n 秒后才执行,舍弃掉上一次事件触发执行操作。简单地说就是频繁的触发事件完毕后的 n 秒内不再触发同一事件,函数才会执行。

函数防抖的实现

实现:根据函数防抖的原理,如果需要使用js代码实现需要 setTimeout()clearTimeout()配合使用。实现方法如下:

// debounce 接受两个参数 func 需要防抖的函数, wait 事件出发完毕后延迟时间
function debounce(func, wait) {
    var timeout; // 使用闭包持久化延时器 id 变量
    return function () {
        clearTimeout(timeout) // 每次抖动执行函数时清楚上一次的延迟函数,从而达到取消上一次函数执行的效果
        timeout = setTimeout(func, wait);
    }
}

container.onmousemove = debounce(getUserAction, 1000);

思考:上面的debounce函数虽然实现了函数的防抖效果,但是他会影响到防抖函数 this 指向问题,因为getUserAction()setTimeout() 触发,导致 getUserAction() 函数内部 this 指向 window 对象。解决 this 指向问题可以通过bind()call()apply()方法修改:

function debounce(func, wait) {
    var timeout; 

    return function () {
        // 返回的函数会作为事件处理函数绑定给对应的监听事件,此时的this指向的是正确的对象
        var context = this;
        // 使用bind创建一个指定this指向为当前对象的新函数并替换原函数
        func = func.bind(context)
        // 每次抖动执行函数时清楚上一次的延迟函数,从而达到取消上一次函数执行的效果
        clearTimeout(timeout)
        // 此时 func 已经是被指定了this的新函数
        timeout = setTimeout(func, wait);
    }
}

container.onmousemove = debounce(getUserAction, 1000);

思考:虽然上面的代码我们解决了this的指向问题,但是绑定的事件回调函数会默认接受 event 事件对象作为参数,提供给开发人员处理相应的操作。所以我们还要对上面的代码进行如下改进,使得防抖函数支持 event 默认参数:

function debounce(func, wait) {
    var timeout; 
    // 返回的函数会作为事件处理函数绑定给对应的监听事件,此时函数会默认接受event事件对象作为参数
    return function (event) {
      
        var context = this;
        // 将 event 事件对象作通过bind方法传递给防抖函数作为参数。
        func = func.bind(context, event)
      
        clearTimeout(timeout)
  
        timeout = setTimeout(func, wait);
    }
}

container.onmousemove = debounce(getUserAction, 1000);

注意:上面的代码显式的获取event事件对象并传递到绑定this的函数中我们可以使用 arguments 解决这一问题

function debounce(func, wait) {
    var timeout; 
  
    return function (event) {
      
        var context = this;

         var args = arguments;
       
        func = func.bind(context, args)
      
        clearTimeout(timeout)
  
        timeout = setTimeout(func, wait);
    }
}

container.onmousemove = debounce(getUserAction, 1000);

函数防抖实现后我们再观察页面效果:
防抖效果实现

「课堂练习」

请对表单元素的用户输入实现防抖

要求:

  1. 表单元素的输入操作设置为防抖函数
  2. 每次用户输入完毕后将输入的结果再表单元素的下方展示

运行效果:
防抖联系
部分代码:

<style>
    input {
        display: block;
        width: 600px;
        height: 50px;
        padding: 0 20px;
        font-size: 20px;
        margin: 100px auto;
    }

.message {
        width: 600px;
        margin: 0px auto;
        font-size: 30px;
        background-color: #ccc;
        padding: 10px 20px;
        color: red;
        height: 30px;
    }
</style>


<input type="text" placeholder="请输入.." class="input">

思考:是否可以拥有返回值

问题:因为 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是因为使用了 setTimeout 延迟执行,导致func始终是异步执行的, 通过 func.bind(context, args) 调用后的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,无法实现防抖函数返回返回值功能

解决方案:使用同步形式给防抖函数提供返回值功能,即防抖函数频繁调用时立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。并通过添加一个 immediate 参数指定是否开启此功能。

// immediate 布尔值是否开启立即触发形式的防抖功能从而实现防抖函数有返回值效果
function debounce(func, wait, immediate) {
    // result 变量保存立即执行后函数的返回值
    var timeout, result;

    return function () {
        var context = this;
        var args = arguments;
        // 优化判断避免第一次调用函数清除上一次不存在的计时器id(虽然不会报错)
        if (timeout) {
            clearTimeout(timeout);
        }
        // 判断是否开启立即执行模式函数防抖
        if (immediate) {
            
            // 只有第一次执行时timeout不存在,callNow为真
            var callNow = !timeout;
            // 指定wait时间内,timeout都不可能为空,直到指定时间结束后timeout值为null,才可以重新触发执行
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)

            // 如果已经执行过,不再执行
            if (callNow) {
                // 使用apply方法指定this并立即执行
                result = func.apply(context, args)
            }
        }
        else {
            // 不使用立即执行模式,没有返回值
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

container.onmousemove = debounce(getUserAction, 1000, true);

查看效果:
立即执行

思考:取消防抖函数

在真实的开发中,函数的防抖应用场景中经常需要取消防抖函数的功能,如:在微博中下拉加载功能(每次用户的下拉加载操作应用都要向服务器发送网络请求获取最新的微博信息)若下拉操作只有等 10 秒后才能重新触发事件,并且可能因为设备网络信号原因需要在10秒内取消本次加载操作的应用场景。

为了实现这一功能,我们需要知道在JavaScript中函数也是一个特殊的对象。所以将取消函数作为防抖函数的取消方法(属性)一并返回就可以实现取消防抖函数的效果

function debounce(func, wait, immediate) {

    var timeout, result;
    // 使用变量debounced接受防抖函数
    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(context, args)
            }
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };
    // 通过变量debounced 给防抖函数添加一个属性方法cancel,该方法内部用来清除延迟函数并且将timeout设置为null
    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };
    // 将防抖函数返回
    return debounced;
}

// 测试取消防抖函数执行
var count = 1;
var container = document.getElementById('container');

function getUserAction(e) {
    container.innerHTML = count++;
};
// 获取设置了防抖的函数
var setUseAction = debounce(getUserAction, 10000, true);
// 将防抖函数绑定给频繁触发的事件
container.onmousemove = setUseAction;

// 将取消指定防抖函数的方法绑定给其他事件
document.getElementById("button").addEventListener('click', function(){
    setUseAction.cancel();
})

效果如图:

取消防抖函数

以上就是函数防抖的所有概念了。

函数的节流

介绍

节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。

函数节流的实现

概念:根据上面的介绍我们可以理解函数的节流就是如果用户持续触发事件,每隔一段时间,只执行一次事件。

在函数的届六中中首次触发是否执行以及结束后是否执行,不同需求,实现的方式也有所不同。目前实现函数节流有两种主流方式:

  1. 使用时间戳
  2. 设置定时器。

我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。

使用时间戳

原理:当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,并更新时间戳为当前的时间戳,如果小于,就不执行。

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

    return function() {
        var now = new Date().getTime();
        // var now = +new Date(); 也可转化为时间戳
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

container.onmousemove = throttle(getUserAction, 1000);

效果演示如下:

时间戳节流

我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。

使用定时器

原理:当触发事件的时候,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行完毕,然后执行函数,清空定时器,再设置下个定时器。

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

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}

效果演示如下:

使用定时器

我们可以看到:当鼠标移入的时候,事件不会立刻执行, 3s 后执行了一次,此后每 3s 执行一次,当数字显示为 3 后,移出鼠标停止触发,但是依然会在第 12s 的时候执行一次事件。

比较两个方法:
  1. 使用时间戳事件会立刻执行,使用定时器事件会在 n 秒后第一次执行
  2. 使用时间戳事件停止触发后没有办法再执行事件,使用定时器事件停止触发后依然会再执行一次事件

优化

需求:开发中我们希望能指定是否立刻执行,或停止触发的时候是否能再执行一次。

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

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

代码实现:

function throttle(func, wait, options) {
    // 计时器id, this, 参数
    var timeout, context, args
    // 上一次的时间
    var previous = 0;
    // 判断是否设置配置选项options
    if (!options) {
        options = {};
    } 
    // 创建节流函数
    var throttled = function() {
        // 获取时间戳
        var now = new Date().getTime();
        // previous 为 0 即第一次调用,且 禁用第一次执行时
        if (!previous && options.leading === false) {
            previous = now;
        }
        // 事件触发间隔与 规定间隔时间(wait) 差值
        var remaining = wait - (now - previous);
        
        context = this;

        args = arguments;
        // 如果没有剩余的时间了
        // 若禁用第一次执行,第一次执行时 remaining = wait 不会进入该判断
        if (remaining <= 0) {
            // 清除上一次计时器
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            // 更新本次时间
            previous = now;

            // 立即调用函数
            func.apply(context, args);
            
            // 清理工作 js 的垃圾回收
            if (!timeout) {
                context = args = null;
            }
          // 还有剩余时间,但是想添加一个最后指定时间的触发回调
        } else if (!timeout && options.trailing !== false) { 
            // 这里是函数防抖
            // !timeout 还未添加回调,已经添加结束回调不执行次代码
            // ptions.trailing !== false 不禁用停止触发的回调
            // 创建最后一次触发回调
            timeout = setTimeout(later, remaining);

            // 清理工作 js 的垃圾回收
            if (!timeout) {
                context = args = null;
            } 
        }
    };

    var later = function() {
        // 事件结束后根据是否立即执行重新设置previous
        // 无需立即执行将previous设置成0
        // 需要立即执行previous设置成本次方法触发的时间
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
    };

    return throttled;
}

注意:上面的代码的实现中有这样一个问题:就是 leading:false 和 trailing: false 不能同时设置。(在真实开发中不可能遇到leading:false 和 trailing: false都需要为false的情况)

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

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});

取消

在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:

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

ug 就出来了,所以,这个 throttle 只有三种用法:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});

取消

在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:

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

以上就是全部函数节流的概念

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JavaScript 函数(debounce)是一种技术,可以函数在短时间内多次触发。 当函数被触发时,函数会延迟执行函数,并且如果在延迟期间内再次触发函数,则会重新计算延迟时间。这样可以避免函数在短时间内多次触发,从而节省资源。 举个例子,假设你有一个函数,用于搜索用户输入的内容。如果每次用户输入都立即触发函数,会导致函数频繁触发,浪费资源。使用函数,可以在用户输入后延迟一段时间才触发函数,从而节省资源。 下面是一个简单的函数的例子: ``` function debounce(fn, delay) { let timer = null; return function () { const context = this; const args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); } } ``` 上面的函数接受两个参数:`fn` 是要进行函数,`delay` 是延迟时间(单位为毫秒)。函数返回一个新函数,在新函数中设置了延迟执行的逻辑。 使用函数的方法如下: ``` const debouncedFunction = debounce(function () { // 在这里 ### 回答2: JavaScript 函数是一种常用的优化技术,旨在减少函数的调用频率。 当一个函数被频繁触发时,机制会在一定的延迟时间内,只执行最后一次触发的函数调用。这样可以避免函数被频繁执行,提高性能和用户体验。 实现函数有两种方式:立即执行和延迟执行。 立即执行的函数,意味着只有当函数被最后一次调用时,才会立即执行函数。延迟执行的函数,意味着只有当函数在一定时间内没有被再次调用时,才会执行函数。 通过使用定时器和闭包,可以实现函数的功能。当函数被触发时,先清除之前设置的定时器,然后重新设置一个延迟执行的定时器。如果在延迟时间内再次触发函数,就会清除之前设置的定时器,重新设置一个新的延迟执行的定时器。只有当延迟时间内没有再次触发函数,定时器才会执行函数函数在多种场景中都有应用。比如在用户输入搜索框时,可以使用函数来减少发送搜索请求的频率;在窗口大小改变时,可以使用函数来优化页面重绘的性能。 总之,JavaScript 函数是一种常用的优化技术,通过减少函数的调用频率来提高性能和用户体验。在实现上,可以通过定时器和闭包来实现函数的功能。 ### 回答3: JavaScript函数是一种提高性能和优化用户体验的技术。它所解决的问题是在一段时间内频繁触发某个函数时,只执行最后一次触发的函数调用。 实现函数的方法是利用定时器。当触发事件时,会开始一个定时器,如果在指定的时间间隔内再次触发了该事件,定时器就会被清除并重新开始计时。这样,只有在最后一次触发事件后的等待时间内没有再次触发,才会执行相应的函数调用。 函数在实际应用中有很多场景。比如,在用户输入搜索框时,可以利用函数来减少发送请求的次数,只在用户停止输入一段时间后才发起实际的搜索请求。又如,在窗口大小调整时,可以利用函数来优化响应式布局,只在窗口停止调整一段时间后才重新计算样式。 以下是一个简单的实现函数的示例代码: ```javascript function debounce(func, delay) { let timerId; return function() { const context = this; const args = arguments; clearTimeout(timerId); timerId = setTimeout(function() { func.apply(context, args); }, delay); }; } // 创建一个函数 const debouncedFunc = debounce(function() { console.log('Function debounced'); }, 300); // 触发函数 debouncedFunc(); // 不会执行 debouncedFunc(); // 不会执行 debouncedFunc(); // 不会执行 // 等待300毫秒后,只执行一次 ``` 以上示例代码中,`debounce`函数接受一个函数和一个延迟时间参数,返回一个新函数,实现了函数的逻辑。在创建函数后,多次调用这个函数,只有停止调用一段时间后才会执行实际的函数逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值