高阶函数应用:函数防抖和函数节流

什么是函数防抖和函数节流

在一些情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就有函数防抖和函数节流。

函数被频繁调用的常见的几个场景

  • mousemove 事件。如果要实现一个拖拽功能,需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置 DOM 的位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死。

  • window.onresize 事件。为 window 对象绑定了 resize 事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果在 window.onresize 事件函数里有一些跟 DOM 节点相关的操作,而跟 DOM 节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。

  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)

  • 搜索联想(keyup事件)

  • 监听滚动事件判断是否到页面底部自动加载更多(scroll事件)

对于这些情况的解决方案就是函数防抖(debounce)或函数节流(throttle),其核心就是限制某一个方法的频繁触发。

函数防抖

函数防抖,是指防止函数在极短的时间内反复调用,造成资源的浪费。

考虑一下电梯关门的场景,现代的大部分电梯都可以通过红外,感知到是否有人进入,为了避免夹到人,同时为了等待后面的人,电梯关门的时间往往有这么一种规则:始终保证电梯门在最后一个人进入后3秒后关闭。如果有人进入后,还没有等到3秒又有人进来了,电梯门会以最后一次进入的时间为计时起点,重新等待3秒。

再考虑一个页面上的场景,页面上的某些事件触发频率非常高,比如滚动条滚动、窗口尺寸变化、鼠标移动等,如果我们需要注册这类事件,不得不考虑效率问题,又特别是事件处理中涉及到了大量的操作,
当窗口尺寸发生变化时,哪怕只变化了一点点,都有可能造成成百上千次对处理函数的调用,这对网页性能的影响是极其巨大的。于是,我们可以考虑,每次窗口尺寸变化、滚动条滚动、鼠标移动时,不要立即执行相关操作,而是等一段时间,以窗口尺寸停止变化、滚动条不再滚动、鼠标不再移动为计时起点,一段时间后再去执行操作,就像电梯关门那样。

再考虑一个搜索的场景(例如百度),当我在一个文本框中输入文字(键盘按下事件)时,需要将文字发送到服务器,并从服务器得到搜索结果,这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验,如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的!想想看,我搜索“google”这样的单词,至少需要按 6 次按键,就这一个词,我需要向服务器请求 6 次,并让服务器去搜索 6 次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。

真正的搜索行为,并不是每次按键都会触发的,只有当用户停止按键一段事件后才会触发。

于是,为了满足这种类型场景,我们可以开发一个通用的函数,这个函数要满足以下功能:

  • 调用该函数后,不立即做事,而是一段时间后去做事
  • 如果在等待时间内调用了该函数,重新计时

这样的功能,就叫做函数防抖,其实就是防止函数短时间内被调用多次。要完成该函数,需要给予两个条件:

  • 告诉我一段时间后要做什么事(这里应该是一个回调函数,即函数作为参数)
  • 告诉我要等待多长时间(毫秒)
// 设置全局变量,记录setTimeout得到的id
let timerId = null;

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, wait){
    if(timerId){
        // 如果有值,说明目前正在等待中,清除它
        clearTimeout(timerId);
    }
    // 重新开始计时
    timerId = setTimeout(() => {
        func();
    }, wait);
}

虽然我们实现了效果,可是仔细想想,目前这个防抖函数还是太年轻,太简单,有时候还很幼稚。

因为它使用了一个全局变量timerId,这意味着,页面上所有需要防抖的地方都要共用这个变量,如果我同时要对窗口尺寸改变事件和文本框按键事件进行防抖,就办不到了。

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, wait){
    //设置变量,记录setTimeout得到的id
    let timerId = null;
    if(timerId){
        //如果有值,说明目前正在等待中,清除它
        clearTimeout(timerId);
    }
    //重新开始计时
    timerId = setTimeout(() => {
        func();
    }, wait);
}

但是这样子还是有问题,这样的话,每次调用防抖函数都会得到一个新的计时器,过去的计时器找不到了。

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, wait){
    //设置变量,记录setTimeout得到的id
    let timerId = null;
    return function(){
        if(timerId){
            //如果有值,说明目前正在等待中,清除它
            clearTimeout(timerId);
        }
        //重新开始计时
        timerId = setTimeout(() => {
            func();
        }, wait);
    }
}

这样一来,debounce 函数仅仅负责生成一个 timerId,保证每次调用 debounce 都会有一个新的 timerId 产生即可,将防抖的工作,交给了返回的函数去完成。

今后如果我要对某一个操作进行防抖,我只需要调用 debounce 函数来得到一个操作函数,后面反复去调用操作函数即可。

目前,我们的防抖函数还无法传递参数给回调函数,只要稍稍做一点改变,即可实现参数的传递:

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, wait){
    // 设置变量,记录 setTimeout 得到的 id
    let timerId = null;
    return function(...args){
        if(timerId){
            // 如果有值,说明目前正在等待中,清除它
            clearTimeout(timerId);
        }
        // 重新开始计时
        timerId = setTimeout(() => {
            func(...args);
        }, wait);
    }
}

之后,我们就可以在调用时传递参数了。

有了防抖函数,我们将来遇到鼠标移动、窗口尺寸改变等需要大量调用同一个函数的时候,就可以使用它来降低执行频率,保证执行效率。

函数节流

函数节流的目的,也是为了防止一个函数短时间内被频繁的触发。

函数防抖的原理是将即将被执行的函数用setTimeout()延迟一段时间执行。而函数节流的原理是让连续的函数执行,变为固定时间段间断地执行。关于节流的实现,有 2 种主流的实现方式,一种是使用时间戳,一种是设置定时器。

1. 使用时间戳

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

function throttle(func, wait) {
    let context, args;
    let previous = 0;
    return function () {
        let now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

2. 使用定时器

触发事件时,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

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

本文章取自本人JS语言导师谢老师的学习总结,同时也感谢谢老师对我的谆谆教诲,感谢他带我走上前端这条道路,并让我为之不断向前

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值