[核心概念] 一文说透函数防抖和节流 (debounce/throttle)

2,250 阅读9分钟

系列开篇

为进入前端的你建立清晰、准确、必要概念和这些概念的之间清晰、准确、必要关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。

面试题

  • 谈谈函数防抖节流
  • 手写防抖节流

这是干什么的?

函数防抖与节流是很相似(但不同)的概念,简单来说就是一个能控制一段时间某个函数的执行次数的方案。用来优化计算机或网络资源。再白话点就是当你的函数高频率执行时能让你的这个方法少执行几次,如果是异步的,少几次网络请求,是不是优化了资源。下面我们分别看下这两个概念。

函数防抖 debounce

简单来说就是把多次执行组合成一次执行。

比如我们设置了一个时间间隔 5 秒,当事件触发的间隔超过 5 秒,(回调)函数才会执行,如果在 5 秒内,事件又被触发,则刷新这个 5 秒,至少5秒后事件没被触发才执行函数。

类比

乘公交车,一直有人陆陆续续上车(事件触发),司机心想30s(时间间隔) 内没人继续上,再开车(函数执行)。提高了公交的资源利用。

乘电梯,程序设定电梯在没有人20s(时间间隔) 内按开门按钮(事件触发)上下电梯,再关门启动(函数执行)。提高了电梯的资源利用。

示意

函数防抖示意 来源 css-tricks.com/

我们开发中可能遇见的场景

注意这就是我上面提到的概念之间的关联,当你开发中遇见了这类问题,对这个概念清晰的认识以及知道其中的关联会让你迅速找到你需要实现的目标和问题的根源。

  • 搜索输入框(Autocomplete),当不再输入后的几百毫秒再去发送请求,减少服务器压力。
  • 注册框(即时判断是否重复用户名),或需要后台校验的文本输入框同理。
  • 提交按钮的点击,有的人就是会疯狂的点,有啥办法呢。
  • 不停改变浏览器窗口大小会触发多次 resize 事件,引起浏览器的重排【关联概念(弱)】,消耗性能。

函数节流 throttle

简单来说 就是在指定的时间间隔内,只允许我们的函数执行一次。

比如一个事件在被疯狂触发,本来每秒执行几百次(回调)函数,而你使用函数节流设了个时间间隔 1s,那么这个函数在1s 内只会执行一次。

类比

日本有种工具叫鹿威就是这玩意。

如果水直接往下面水池倒,频率非常高(不间断),但是有这个工具就控制了水往下倒的频率,隔一段时间倒一次。和throttle比较像。

与防抖之间的主要区别

节流至少每时间间隔内 保证有规律地执行该功能。

比较图 demo

我们开发中可能遇见的场景

  • 无限滚动列表。用户向下滚动列表时,您需要时刻检查用户屏幕离底部有多远。如果用户接近底部,我们应该请求下一页内容并将其附加到页面上。为什么不用 debounce 因为它仅在用户停止滚动时才会触发。我们需要在用户到达底部之前就开始获取内容。throttle可以保证我们一直在检查距底部的距离。但不用频率过高的执行函数(scroll 事件触发的回调)。
  • 高频点击提交按钮,比如你抢票的时候,用debounce你就会因为太想抢而不停地点确越抢不到。
  • 监听鼠标 mousemove 计算一个div跟随鼠标移动而移动的函数等等, 用debounce就很不连贯

怎么用?

讲道理这个我建议直接用lodash (省的自己写,而且有些变体用法满足多种需求比如 debounce 的前置执行还是后置执行参数)

debounce

throttle

这个专栏不是直接能复制代码用项目中的文档,搜索引擎使用方式的锻炼也及其重要。主要目的是弄清概念,理清关联。遇到问题知道如何去往何处找方法,不纠结在问题本身。

实现原理是什么?

我们深刻理解了这个概念之后,可以探究下它的实现(面试也经常问到这方面源码),可能有人觉得没啥用,我觉得它的用处是拓展出其他相关联的【必知】概念,也可以看看你的硬编码能力,再不济看看你的记忆力如何也是好的。(^-^)

debounce

我们先写出一个写法,可复制下面的代码到浏览器试一波。

//---------------------------------测试用例--------------------------------
// 用户高频率执行的函数(需要防抖的函数),但可能是个异步请求列表,成本比较高需要优化
function userHighRequencyAction(e, content) {
    console.log(e, content);
}

// 给这个高频的方法,加防抖方案输出一个防抖的function
var userDebounceAction = debounce(userHighRequencyAction, 1000);

// 如何触发那个高频函数 绑定一个onmousemove事件,来模拟高频触发  $(1)事件监听$
document.onmousemove = function (e) {
    userDebounceAction(e, 'test debounce'); // 给防抖函数传个参
}
//---------------------------------实现-----------------------------------

function debounce(func, wait) {              // -----> $(2)作用域和闭包$
    let timer; 
    return function () { 
        let context = this;                 
        let args = arguments;
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            func.apply(context, args);       // -----> $(3)this$
            // 其实就是 context.func(args)
        }, wait);
    };
}

看了上面的实现即使有解释,我想你也是云里雾里,为什么要这么做?有什么好处?其实是你需要理解清楚一些其他必要的概念。和那些必要概念与之的必要关联。看不懂没关系,多看两遍,多思考关联,知识是一张网,继续下去,你就能把知识融汇贯通。

关联概念

理解了这些概念后,再来简单说明下这实现思路。你会觉得,下面我这段话异常清晰直接,如果觉得模糊,再多在关联概念里游会,回来后你会变得更健壮。

简单说明

其实这其中逻辑没什么好说的,就是利用 setTimeout 这个WebApi的延迟效果,设置一个定时器,当没有到达延迟时间就清除定时器(clearTimeout),并建立一个新的定时器,继续等延迟时间,如此循环。

重点来看几个疑问

  1. 为什么要返回函数或者说为什么要用 闭包【关联概念】。其实debounce函数只调用了一次,后面调用的全是闭包函数,其实了解闭包都知道这样 timer 定时器的变量不被gc回收,这样下次执行时仍然指向的是上一次设置的定时器。

  2. 为什么要绑定 this, 因为fn 执行的时候this指向全局对象(浏览器中是window),根据词法作用域,可以在外层用个变量保存下 this, 再用 apply 进行显示绑定。

  3. 为什么要有 arguments 因为 JavaScript 在事件处理函数中会提供事件对象 event, 所以我们得把参数一并传入, 而apply/call是可以传参的(具体自查MDN的api)。

这样理解是不是轻松多了,所以对概念之间关联的理解跟清晰的概念本身同样重要。

throttle

function throttle(func, wait) {
    var timer, 
    	context, 
        args;
    return function () { 
        context = this;                 
        args = arguments;
        if (!timer) {
          timer = setTimeout(function() {
            // 执行后置定时器变量为null
            timer = null;
            func.apply(context, args);
          }, wait);
        }
    };
}

简单来说,当触发事件的时候,设置一个 timer, 再次触发事件的时候 timer 存在(不为null),则不执行,直到函数执行了,把timer置空,并启动设置下一个定时器。这也就保证了 wait时间内函数只会执行一次。

其实这是我的第一篇文章,本来只是想记录下这个知识点,但是写起来,却是第10篇才完篇的。这让我深刻感受到知识不是孤岛,相互关联才更具力量。所以读的时候也别纠结一定要一篇篇读"完",因为里面是各种信息环路。你要做的是把这张网在你脑子里拉起来(概念网点)注意别有漏洞(概念不清)。把这张网的线(关联)尽量变粗变结实。

其他

在你快速阅读时,这部分可以不看,甚至有些弱关联的概念也可以简单带过,抓住你本次想了解的核心内容,不纠结把文章看完,而是一次弄清一个概念,反复咀嚼消化,直至你用起来得心应手,知其所以然。

requestAnimationFrame(rAF)

限制函数执行速率的另一种方法。 根据经验,如果你的JavaScript函数是“绘画”或直接对动画属性进行实时处理或重新计算元素位置使用requestAnimationFrame会是好选择。 MDN

debounce 手写简单拓展

虽然lodash 现成 api 但可能面试会问就简单列下这种举一反三的问题,不过你当时想想其实问题不大。例子就用别人现成的了。

需求: 不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

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(context, args)
            }
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

参考