• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

五一不休息 天天学习 教你从零开始写


Recommended Posts

jlifnuw41ai3790.jpg

壹 引

我在《手写从零开始的去抖方法》一文中详细介绍了防抖的概念以及如何手写一个防抖。既然自然要谈防抖,就不能回避同样重要的节流,作为老规矩。先解释一下节流的概念,能解决什么场景问题,再由浅入深写一个相对完善的节流方法,于是本文开始。

贰 节流场景与概念理解

以免在学习节流的时候混淆节流和防抖的概念,在介绍节流之前,我们先来回顾一下防抖是用来解决什么场景问题的。

说到防抖,可以想到上面提到的电梯关门问题。对于一个连续的操作,防抖侧重于操作之间的等待间隔。如果操作后等待时间不再继续,那么我们将做一件事。节流不同于防抖。它不关注用户停什么,而是致力于优化一段时间内不得不做的高频操作场景,比如onscroll和onmousemove事件监控。

你可以想象这个要求。我们需要感知当前滚动条和被监控元素顶部之间的距离。这时候只要我们滚动滚轮,你会发现监控事件会被连续调用。假设监控事件中有非常复杂的逻辑计算,那么每次调用都会造成很大的性能消耗。

div class='节流'

进入lorem后,按TAB键自动填充,这样这个div就有滚动条了。

/div

英国铁路公司

span class='span'/span

const div=document . getelementsbyclassname(' throttle ')[0];

const span=document . getelementsbytagname(' span ')[0];

const setScrollTop=function () {

Span.innerHTML=`到滚动条顶部的距离是:$ { div . scroll top } `;

};

div . on scroll=setscroll top;

2mugdkp0apk3791.gif

这里有一个填充随机文本的快捷方式。在html标签中输入lorem,按TAB键随机生成一段文字。填内容很方便。

所谓节流,相当于给这种高频事件加了一个执行限制器。原来只需要滚动就能触发,现在滚动过程中每等待一次才触发一次。UI层还是可以根据用户的操作进行及时的反馈,只是反馈的频率没有那么高了。

为了加深对节流的理解,我们举一个贴近生活的例子。记得小时候农村很多人用的都是井水。后来随着自来水的普及,水管和水表走进了每家每户,水龙头打开时水流的速度会影响水表的转速。但是聪明的劳动人民发现,如果不把水龙头完全拧紧,让它匀速缓慢滴水,不仅会带动水表转动,甚至一天会滴出半桶水,这其实就是节流。

同样,在地铁早晚高峰时段,为了减少地铁内的拥堵和安全压力,地铁工作人员会在地铁入口处设置一个风门,一段时间内只放一批人进去。

好了,我们介绍了节流的概念和解题场景,最后总结了节流和防抖的区别:

防抖:我关心的是操作之间的间隔,间隔时间是否等于=等待,如果是,我给你做一次。

节流:我们关注操作的过程。我们在这个高频率的进程中添加了一个执行限制器,这样在固定的等待时间内只能执行一次。

叁 从零实现一个节流throttle

叁 壹 使用时间戳实现节流

既然知道了节流的概念,那就来实现自己的节流吧。目的很简单。添加一个时间限制器,在每次等待时执行。怎么做?更直观的方法是借用时间戳。我们假设最后一次执行的时间戳是0,然后在第二次执行时得到当前时间戳,再用第二次时间戳减去最后一次,看是否=wait。如果满足,就执行,否则不执行。

关于时间戳有个小技巧。我们知道new Date()可以获得当前时间,这比

如:
new Date(); // Sun May 01 2022 17:32:31 GMT+0800 (中国标准时间)

但很明显这个时间并不能用于计算,这里我们可以借用javascript的隐式转换,在前面添加一个+,比如:

+new Date(); // 1651397686563

+在这里的目的是将Date对象转换成原始值,只是对于Date而言,+的行为会让Date在底层执行了toPrimitive操作,感兴趣可参考Date.prototype[@@toPrimitive],这里你只用知道就是转数字即可。日常开发中我们也有用+将字符串转为数字的做法,比如+'1''1'转成数字类型1

typeof +'1'; // number

反过来数字转成字符串:

typeof (1 + '');// string

题外话了,现在来利用时间戳实现节流:

const div = document.getElementsByClassName('throttle')[0];
const span = document.getElementsByTagName('span')[0];
const throttle = (fn, wait) => {
  let pre = 0;
  // 返回闭包
  return function (...args) {
    // 绑定this
    const this_ = this;
    const cur = +new Date();
    if (cur - pre >= wait) {
      // 更新pre的时间
      pre = cur;
      // 执行真正的方法
      fn.apply(this_, args);
    }
  }
};
const setScrollTop = function (e) {
  span.innerHTML = `滚动条距离顶部距离为:${e.target.scrollTop}`;
};
// 通过节流生成节流方法
const setScrollTop_ = throttle(setScrollTop, 300);
// 替换原有方法为新生成的节流方法
div.onscroll = setScrollTop_;
isjw43wmcfh3792.gif

实现中关于绑定this以及接受参数args的做法,在防抖一文已经详细解释了,这里就不再赘述,如果还有不理解可以留言我再做解释。

可以看到现在滚动滚动条,更新同样会调用,但是每隔wait执行一次,并不会那么频繁了,但是问题也很明显,因为间隔的检查,可能我最后一次的操作恰好在这个wait时间内,导致不满足条件,从而未能做最后一次更新,可以看到上图中我的滚动条最后滚到顶部,而距离并没有被更新成0

叁 ❀ 贰 使用定时器实现节流

我们再来介绍第二种实现方法,借用定时器setTimeout的做法,这种做法与防抖思想有一定相似之处。我们假定一开始定时器为空,滚动滚轮,然后创建一个wait后执行的定时器,只要这个定时器不执行就不允许创建定时器,而定时器执行时我们会手动把定时器ID置空,这样就保证wait时间内一定只存在一个定时器,从而达到wait时间执行一次的目的。

怕有同学不能理解,我们在脑中模拟下这个过程:

第一次操作:time为空,创建定时器let time = setTimeout(),它会在wait时间后执行。

第二次操作:假设这个操作间隔仍在wait之内,所以time还没执行,因此我们不创建新的定时器。

第三次操作:假设这个操作的时间间隔已经超过了wait,那么time一定执行过了,我们提前在time内定义time = null的操作,保证定时器自己执行后清空定时器ID

思路非常清晰了,实现这个代码:

const throttle = (fn, wait) => {
  let time;
  return function (...args) {
    const this_ = this;
    // 如果存在time则不创建新的定时器
    if (!time) {
      time = setTimeout(() => {
        // 定时器会在wait后执行,执行完毕清空time
        time = null;
        fn.apply(this_, args);
      }, wait);
    }
  }
}
ya2qktreoux3793.gif

同样能达到节流的效果,但是它的问题也很明显,因为定时器的存在,第一次执行一定是wait之后才会执行,假设wait较大,第一次操作的UI呈现延迟就非常明显,但也因为定时器延迟效果,你会发现我们总能正确获取最后一次操作的高度。

为什么能获取最后一次的高度?因为dom也是对象,也存在引用关系,要么你当时就触发了定时器得到高度,没触发的也会等wait之后获取最新的高度。

简单点理解,假设我滚动到底不动了,假设这是上一次创建的定时器,因为dom的引用关系,我一样能拿到最新的高度。假设这是滚动到底才创建的新定时器,一样wait后获取到最新高度。

那么问题来了,时间戳能实时响应第一次操作的反馈,但是无法感知最后一次;而定时器做不到第一次操作的实时反馈,但总能获取到最后一次操作的反馈,我们能不能综合下这两者,让第一次和最后一次都能得到反馈呢?

叁 ❀ 叁 解决第一次与最后一次响应问题

通过上面的分析,我们可以得知,如果要感知最后一次操作,定时器肯定不能少,但有了定时器,你又要等待wait之后才能执行,这里就有点相互矛盾了。

我们其实可以简单点理解,假设cur - pre > wait,那表明当前可以立刻执行,假设并没有超过wait,考虑到之后我们还得执行,那我们就设置定时器,等wait后执行,我们先实现大致的框架:

const throttle = (fn, wait) => {
  let time;
  let pre = 0;
  return function (...args) {
    // 保存this
    const this_ = this;
    // 获取当前时间
    const cur = +new Date();
    // 如果时间差>=wait,执行
    if (cur - pre >= wait) {
      pre = cur;
      fn.apply(this_, args);
      // 考虑到之前可能还有定时器没执行,清除定时器并重置id
      if (time) {
        clearTimeout(time);
        time = null;
      };
    } else if (!time) {
      time = setTimeout(() => {
        // 记录当前最新的时间
        pre = +new Date();
        // 定时器会在wait后执行,执行完毕清空time
        time = null;
        fn.apply(this_, args);
      }, wait);
    }
  }
}

在这个结构中,我们相当于简单粗暴的综合了前面两种实现思路,超过了等待时间我们就执行,没超过那我就记录一个定时器等待wait后执行。同时,在立刻执行时,假设之前还有没跑完的定时器,为了避免重复执行,我们顺手清除定时器,以及重置定制器ID,而定时器执行时,我们考虑到下次的立刻执行,因此也顺手更新pre时间,可以说是两者相互成就了。

那这个实现有问题吗?大家可以先尝试思考下。其实是有问题的,只是说影响不大。

我们假定时间间隔是300ms,而某一次的计算cur - pre 等于200ms,这说明我们还不能执行,因此走了定时器的路线,那么问题来了,我们现在定时器等待时间又是wait,还得再等300ms才能走,正确的预期,其实是只用等待100ms就应该执行了。

因此,我们应该额外定义一个剩余执行时间,如果剩余执行时间<=0,那么就立即执行,如果不是,那么这个剩余时间应该成为接下来定时器的等待时间,这样改一改,就相当完美了。

那么我们怎么计算剩余时间呢?其实很简单,wait - (cur - pre)不就是剩余时间了。大家可以把我前面200ms的例子带入思考下,如果能立即执行,这个公式得出的时间一定是<=0,只要大于0,那这个时间就是剩余定时器的等待时间。

OK,我们再次改写代码为:

const throttle = (fn, wait) => {
  let time;
  let pre = 0;
  return function (...args) {
    // 保存this
    const this_ = this;
    // 获取当前时间
    const cur = +new Date();
    // 每次都计算剩余时间
    const remaining = wait - (cur - pre);
    // 不用等了就直接执行
    if (remaining <= 0) {
      pre = cur;
      fn.apply(this_, args);
      // 考虑到之前可能还有定时器没执行,清除定时器并重置id
      if (time) {
        clearTimeout(time);
        time = null;
      };
    } else if (!time) {
      time = setTimeout(() => {
        // 记录当前最新的时间
        pre = +new Date();
        // 定时器会在wait后执行,执行完毕清空time
        time = null;
        fn.apply(this_, args);
        // 根据剩余时间执行定时器
      }, remaining);
    }
  }
}
oim3oasrjia3794.gif

这时候看效果是不是就非常完美了呢?老实说,思考着怎么引入和解释剩余时间的概念,我躺床上想了半小时....也不知道这个解释大家能否理解。

肆 ❀ 总

那么到这里,我们详细介绍了节流的基本概念,以及如何实现一个最基础的节流,站在不同实现的节流上,我们不断抛出问题,解决问题,也最终实现了一个相对完善的节流,大家若对于文中存在疑虑,也欢迎大家留言,我会一一解答,那么到这里本文结束。

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now