老生常谈的函数防抖与节流

发表于 3年以前  | 总阅读数:379 次

前言

本篇文章内容,或许早已是烂大街的解读文章。不过参加几场面试下来发现,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新吧。

文章从防抖、节流的原理说起再结合实际开发的场景,分别逐步实现完整的防抖和节流函数

函数防抖

  • 原理:当持续触发一个事件时,在n秒内,事件没有再次触发,此时才会执行回调;如果n秒内,又触发了事件,就重新计时

  • 适用场景:

  • search远程搜索框:防止用户不断输入过程中,不断请求资源,n秒内只发送1次,用防抖来节约资源

  • 按钮提交场景,比如点赞,表单提交等,防止多次提交

  • 监听resize触发时, 不断的调整浏览器窗口大小会不断触发resize,使用防抖可以让其只执行一次

  • 辅助理解:在你坐电梯时,当一直有人进电梯(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。

下面我们先实现一个简单的防抖函数,请看栗子:

// 简单防抖函数
const debounce = (fn, delay) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    timer = setTimeout(function () {
      fn.call(context, ...args);
      //等同于上一句 fn.apply(context, args)
    }, delay);
  };
};

// 请求接口方法
const ajax = (e) => {
  console.log(`send ajax ${new Date()} ${e.target.value}`);
};

// 绑定监听事件
const noneDebounce = document.getElementsByClassName("none_debounce")[0];
const debounceInput = document.getElementsByClassName("debounce")[0];

noneDebounce.addEventListener("keyup", ajax);
debounceInput.addEventListener("keyup", debounce(ajax, 500));

运行效果如下:

点击这里,试试效果点击demo

可以很清晰的看到,当我们频繁输入时, 不使用节流就会不断的发送数据请求,但是使用节流后,只有当你在指定间隔时间内没有输入,才会执行发送数据请求的函数。

上面有个注意点:

  • this指向问题,在定时器中如果使用箭头函数()=>{fn.call(this, ...args)} 与上面代码效果一样, 原因时箭头函数的this是「继承父执行上下文里面的this」

关于防抖函数的疑问:

  1. 为什么要使用 fn.apply(context, args), 而不是直接调用 fn(args)

如果我们不使用防抖函数debounce时, 在ajax函数中打印this的值为dom节点:

<input class="debounce" type="text">

在使用debounce函数后,如果我们不使用fn.apply(context, args)修改this的指向, this就会指向window(ES6下为undefined)

2 . 为什么要传入arguments参数

我们同样与未使用防抖函数的场景进行对比

const ajax = (e) =>{
    console.log(e)
}

3 . 怎么给ajax函数传参

有的小伙伴就说了, 你的ajax只能接受绑定事件的参数,不是我想要的,我还要传入其他参数,so easy!

const sendAjax = debounce(ajax, 1000)
sendAjax(参数1, 参数2,...)

因为sendAjax 其实就是debounce中return的函数, 所以你传入什么参数,最后都给了fn

在未使用时,调用ajax函数对打印一个KeyboardEvent对象

使用debounce函数时,如果不传入arguments, ajax中的参数打印为undefined,所以我们需要将接收到的参数,传递给fn

函数防抖的理解:

❝我个人的理解其实和平时上电梯的原理一样:当一直有人进电梯时(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。

从上面的例子,对防抖有了初步的认识,但是在实际开发中,需求往往要更加的复杂,比如我们要提交一个表单按钮,为了防止用户多次提交表单,可以使用节流, 但如果使用上面的节流,就会导致用户停止连续点击才会提交,而我们希望让用户点击时,立即提交, 等到n秒后,才可以重新提交。

对上面的代码进行改造,得到立即提交版:

const debounce = (fn, delay, immediate) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
        //等同于上一句 fn.apply(context, args)
      }, delay);
    }
  };
};

从上面的代码可以看到,通过immediate 参数判断是否是立刻执行。

timer = setTimeout(function () {
    timer = null
}, delay)

立即执行的逻辑中,如果去掉上面这小段代码, 也是立即执行,但是之后就不会再执行提交了,当我们提交失败了怎么办(哭),所以加上上面这段代码,在设定的时间间隔内,将timer设置为null, 过了设定的时间间隔,可以再次触发提交按钮的立即执行,这才是完整的。

这是一个使用立即提交版本的防抖实现的了一个提交按钮demo

目前我们已经实现了包含非立即执行立即执行功能的防抖函数,感兴趣的小伙伴可以和我一起继续探究一下去,完善防抖函数~

❝做直播功能时,产品的小伙伴给提出这样一个需求:

直播的小窗口可以拖动, 点击小窗口以及拖动时, 显示关闭小窗口按钮,当拖动结束2s后, 隐藏关闭按钮;当点击关闭按钮时, 关闭小窗口

页面原型如下图所示:

分析需求, 我们可以使用防抖来实现, 用户连续拖动小窗口过程中, 不执行隐藏关闭按钮,拖动结束后2s才执行隐藏关闭按钮;但是点击关闭按钮后,我们希望可以取消防抖, 所以需要继续完善防抖函数, 使其可以被取消。

「可取消版本」

const debounce = (fn, delay, immediate) => {
  let timer, debounced;
  // 修改--
  debounced = function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
      }, delay);
    }
  };

  // 新增--
  debounced.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
};

从上面代码可以看到,修改的地方是将return的函数赋值给debounced对象, 并且给debounced扩展了一个cancel方法, 内部执行了清除定时器timer, 并且将其设置为null; 为什么要将timer设置为null呢? 由于debounce内部形成了闭包, 避免造成内存泄露

上面的需求我写了个小demo, 需要的小伙伴可以看看可取消版本demo, 效果如下所示:

这个demo中,在拖拽过程中还可以使用节流,减少页面重新计算位置的次数,在下边学完节流,大家不妨试试

介绍节流原理、区别以及应用

前面学习了防抖,也知道了我们为什么要使用防抖来限制事件触发频率,那我们接下来学习另一种限制的方式节流(throttle)

函数节流

  • 原理:当频繁的触发一个事件,每隔一段时间, 只会执行一次事件。

  • 适用场景:

  • 拖拽场景:固定时间内只执行一次, 防止高频率的的触发位置变动

  • 监听滚动事件:实现触底加载更多功能

  • 屏幕尺寸变化时, 页面内容随之变动,执行相应的逻辑

  • 射击游戏中的mousedown、keydown时间

  • 辅助理解:

下面我们就来实现一个简单的节流函数,由于每隔一段时间执行一次,那么就需要计算时间差,我们有两种方式来计算时间差:一种是使用时间戳,另一种是设置定时器

使用时间戳实现

function throttle(func, delay) {
  let args;
  let lastTime = 0;

  return function () {
    // 获取当前时间
    const currentTime = new Date().getTime();
    args = arguments;
    if (currentTime - lastTime > delay) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

使用时间搓的方式来实现的思路比较简单,当触发事件时,获取当前时间戳,然后减去之前的时间戳(第一次设置为0), 如果差值大于设置的等待时间, 就执行函数,然后更新上一次执行时间为为当前的时间戳,如果小于设置的等待时间,就不执行。

使用定时器实现

下面我们来看使用定时器实现的方式:与时间戳实现的思路是有差别的, 我们在事件触发时设置一个定时器, 当再次触发事件时, 如果定时器存在,就不执行;等过了设置的等待时间,定时器执行,我们需要在定时器执行时,清空定时器,这样就可以设置下一个定时器了

function throttle1(fn, delay) {
  let timer;
  return function () {
    const context = this;
    let args = arguments;

    if (timer) return;
    timer = setTimeout(function () {
      console.log("hahahhah");
      fn.apply(context, args);

      clearTimeout(timer);
      timer = null;
    }, delay);
  };
}

虽然两种方式都实现了节流, 但是他们达到的效果还是有一点点差别的,第一种实现方式,事件触发时,会立即执行函数,之后每隔指定时间执行,最后一次触发事件,事件函数不一定会执行;假设你将等待时间设置为1s, 当3.2s时停止事件的触发,那么函数只会被执行3次,以后不会再执行。

第二种实现方式,事件触发时,函数不会立即执行, 需要等待指定时间后执行,最后一次事件触发会被执行;同样假设等待时间设置为1s, 在3.2秒是停止事件的触发,但是依然会在第4秒时执行事件函数

总结

对两种实现方式比较得出:

  1. 第一种方式, 事件会立即执行, 第二种方式事件会在n秒后第一次执行
  2. 第一种方式,事件停止触发后,就不会在执行事件函数, 第二种方式停止触发后仍然会再执行一次

接下来我们写一个下拉加载更多的小demo来验证上面两个节流函数:点击查看代码

let state = 0 // 0: 加载已完成  1:加载中  2:没有更多
let page = 1
let list =[{...},{...},{...}]

window.addEventListener('scroll', throttle(scrollEvent, 1000))

function scrollEvent() {
    // 当前窗口高度
    let winHeight =
        document.documentElement.clientHeight || document.body.clientHeight

    // 滚动条滚动的距离
    let scrollTop = Math.max(
        document.body.scrollTop,
        document.documentElement.scrollTop
    )

    // 当前文档高度
    let docHeight = Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight
    )
    console.log('执行滚动')

    if (scrollTop + winHeight >= docHeight - 50) {
        console.log('滚动到底部了!')
        if (state == 1 || state == 2) {
            return
        }
        getMoreList()
    }
}

function getMoreList() {
    state = 1
    tipText.innerHTML = '加载数据中'
    setTimeout(() => {
        renderList()
        page++

        if (page > 5) {
            state = 2
            tipText.innerHTML = '没有更多数据了'
            return
        }
        state = 0
        tipText.innerHTML = ''
    }, 2000)
}

function renderList() {
    // 渲染元素
    ...
}

使用第一种方式效果如下:

一开始滚动便会触发滚动事件, 但是在滚动到底部时停止, 不会打印"滚动到底部了"; 这就是由于事件停止触发后,就不会在执行事件函数

使用第二种方式, 为了看到效果,将事件设置为3s, 这样更能直观感受到事件函数是否立即执行:

// window.addEventListener('scroll', throttle(scrollEvent, 1000))
window.addEventListener('scroll', throttle1(scrollEvent, 3000))

一开始滚动事件函数并不会被触发,而是等到3s后才触发;而当我们快速的滚动到底部后停止滚动事件, 最后还是会执行一次

上面的这个例子是为了辅助理解这两种实现不方式的不同。

时间戳 + 定时器实现版本

在实际开发中, 上面两种实现方案都不满足我们的需求,我们希望一开始滚动就立即执行,停止触发的时候也还能执行一次。结合时间搓方式和定时器方式实现如下:

function throttle(fn, delay) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    if (currentTime - lastTime > delay) {
      // 防止时间戳和定时器重复
      // -----------
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      // -----------
      fn.apply(context, args);
      lastTime = currentTime;
    }
    if (!timer) {
      timer = setTimeout(() => {
        // 更新执行时间, 防止重复执行
        // -----------
        lastTime = new Date().getTime();
        // -----------
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

使用演示效果如下:

实现思路是结合两种实现方式,同时避免两种方式重复执行, 所以当调用时间戳执行函数时,需要将定时器清空;当使用到定时器执行函数时,需要增加修改执行记录的时间lastTime

我们可以看到,开始滚动立即会打印页面滚动了,停止滚动后,时间会再执行一次,滚动到底部时停止,也会执行到滚动到底部了

最终完善版

上面的节流函数满足了我们的基本需求, 但是我们可以进一步对节流函数进行优化,使得节流函数可以满足下面三种情况:

  • 事件函数立即执行,并且事件停止后再执行一次(以满足)
  • 事件函数立即执行,但是事件停止后不再执行(待探究)
  • 事件函数不立即执行,但是事件停止后再执行一次(待探究)

❝注意点:事件函数不立即执行,事件停止不再执行一次 这种情况不能满足,在后面从代码角度会做分析。

我们设置两个参数startlast分别控制是否立即执行与最后是否执行;修改上一版代码, 实现如下:

function throttle(fn, delay, option = {}) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    // 增加是否立即执行判断
    if (option.start == false && !lastTime) lastTime = currentTime;

    if (currentTime - lastTime > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(context, args);
      lastTime = currentTime;
    }
    // 增加最后是否再执行一次判断
    if (!timer && option.last == true) {
      timer = setTimeout(() => {
        // 确保再次触发事件时, 仍然不立即执行
        lastTime = option.start == false ? 0 : new Date().getTime();
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

上面代码就修改了三个地方,一个是立即执行之前增加一个判断:

if (option.start == false && !lastTime) lastTime = currentTime

如果传入参数是非立即执行, 并且lastTime为0, 将当前时间戳赋值给lastTime, 这样就不会进入 if (currentTime - lastTime > delay)

第二个修改地方, 增加最后一次是否执行的判断:

// 原来
// if (!timer) {...}

// 修改后
if (!timer && option.last == true) {
   ...
}

当传入last为true时,才使用定时器计时方式, 反之通过时间戳实现逻辑即可满足

第三个修改的地方, 也是容易被忽视的点, 如果start传入false,last传入true(即不立即执行,但最后还会执行一次), 需要在执行定时器逻辑调用事件函数时, 将lastTime设置为0:

// 确保再次触发事件时, 仍然不立即执行
lastTime = option.start ==false? 0 : new Date().getTime()

这里解决的是再次触发事件时, 也能保证不立即执行。

疑问点

相信有的小伙伴会存在疑问,为什么没有讨论不立即执行, 最后一次也不执行的情况呢(即 start为true, last为true), 因为这种情况满足不了。

当最后一次不执行, 也就不会进入到 定时器执行逻辑,也就无法对 lastTime重置为0,所以,当再一次触发事件时,就会立即执行,与我们的需求矛盾了。关于这一点,大家了解即可

到这里,我们的节流函数功能就差不多了, 如果有兴趣的小伙伴可以自己实现一下可取消功能, 与防抖函数实现方式一致, 这里就不赘述了

「参考资料:」

防抖(Debounce) & 节流(Throttle)

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/BcsuTPH6BUNEXFw13Uvmlg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237297次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8136次阅读
 目录