壹 ❀ 引

我在 从零教你手写实现一个防抖debounce方法 一文中详细的介绍了防抖概念,以及如何手写一个防抖。既然聊到防抖那自然避不开同等重要的节流throttle,老规矩,我们先阐述节流的概念,以及它能解决什么场景问题,再次之后再由浅至深来手写实现一个相对完善的节流方法,那么本文开始。

贰 ❀ 节流场景与概念理解

以免大家在学习节流时,混淆节流与防抖的概念,所以在介绍节流之前,我们先回顾下防抖用于解决什么场景问题。

谈到防抖,大家就可以想到在上文中我们提到的电梯关门问题,对于一段连续的操作,防抖关注的是操作之间的等待间隔,如果操作后wait时间没继续操作,那我们就去做一件事。而节流与防抖不同,它不关注用户什么停止操作,而是专门用于优化一段时间内不得不做的高频操作场景,比如onscroll、onmousemove事件监听。

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

<div class="throttle">
输入lorem后按TAB键自动填充,让这个div有滚动条即可。
</div>
<br>
<span class="span"></span>
const div = document.getElementsByClassName('throttle')[0];
const span = document.getElementsByTagName('span')[0]; const setScrollTop = function () {
span.innerHTML = `滚动条距离顶部距离为:${div.scrollTop}`;
}; div.onscroll = setScrollTop;

这里再教大家一个填充随机文本的快捷方式,在html标签内输入lorem后按下TAB键即可随机生成一段文本,用来做内容填充很方便。

而所谓节流,就相当于我们为这种高频事件增加一个执行限制器,原来是你只要滚动就触发,而现在是滚动过程中每wait时间只触发一次,UI层依旧能根据用户的操作及时反馈,只是反馈的频率没那个高而已。

为加深对于节流的理解,我们还是举一个贴近生活的例子。我记得小时候在乡下用水很多人是靠取井水,后来自来水的普及,水管和水表走进了家家户户,打开水龙头水流的速度会影响水表的转速,而聪明的劳动人民发现,只要将水龙头不完全拧紧,让它匀速的慢慢滴,不仅不会带动水表转动,一天甚至能滴出半桶水,而这种行为其实也是节流。

同样,地铁早晚高峰期,为了降低地铁内的拥挤程度与安检压力,地铁工作人员也会在地铁入口设置节流,一段时间只会放进一批人。

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

防抖:关注的是操作之间的间隔,这个间隔时间是否>=wait,若满足就帮你执行一次。

节流:关注的是操作的过程,我们为这个高频过程添加执行限制器,让它固定wait时间只能执行一次。

叁 ❀ 从零实现一个节流throttle

叁 ❀ 壹 使用时间戳实现节流

既然已经了解了节流的概念,现在我们来实现一个自己的节流。目的很简单,增加一个时间限制器,每wait执行一次,怎么做?比较直观的做法是借用时间戳,我们假定上一次执行的时间戳是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_;

实现中关于绑定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);
}
}
}

同样能达到节流的效果,但是它的问题也很明显,因为定时器的存在,第一次执行一定是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);
}
}
}

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

肆 ❀ 总

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

五一不休息,每天都学习,从零教你手写节流throttle的更多相关文章

  1. 【深度学习系列】PaddlePaddle之手写数字识别

    上周在搜索关于深度学习分布式运行方式的资料时,无意间搜到了paddlepaddle,发现这个框架的分布式训练方案做的还挺不错的,想跟大家分享一下.不过呢,这块内容太复杂了,所以就简单的介绍一下padd ...

  2. SVM学习笔记(二)----手写数字识别

    引言 上一篇博客整理了一下SVM分类算法的基本理论问题,它分类的基本思想是利用最大间隔进行分类,处理非线性问题是通过核函数将特征向量映射到高维空间,从而变成线性可分的,但是运算却是在低维空间运行的.考 ...

  3. 【Keras案例学习】 多层感知机做手写字符分类(mnist_mlp )

    from __future__ import print_function # 导入numpy库, numpy是一个常用的科学计算库,优化矩阵的运算 import numpy as np np.ran ...

  4. 深度学习(一):Python神经网络——手写数字识别

    声明:本文章为阅读书籍<Python神经网络编程>而来,代码与书中略有差异,书籍封面: 源码 若要本地运行,请更改源码中图片与数据集的位置,环境为 Python3.6x. 1 import ...

  5. MindSpore手写数字识别初体验,深度学习也没那么神秘嘛

    摘要:想了解深度学习却又无从下手,不如从手写数字识别模型训练开始吧! 深度学习作为机器学习分支之一,应用日益广泛.语音识别.自动机器翻译.即时视觉翻译.刷脸支付.人脸考勤--不知不觉,深度学习已经渗入 ...

  6. 深度学习之 mnist 手写数字识别

    深度学习之 mnist 手写数字识别 开始学习深度学习,先来一个手写数字的程序 import numpy as np import os import codecs import torch from ...

  7. [学习线路] 零基础学习hadoop到上手工作线路指导(初级篇)

    about云课程最新课程Cloudera课程   零基础学习hadoop,没有想象的那么困难,也没有想象的那么容易.在刚接触云计算,曾经想过培训,但是培训机构的选择就让我很纠结.所以索性就自己学习了. ...

  8. Python学习课程零基础学Python

    python学习课程,零基础Python初学者应该怎么去学习Python语言编程?python学习路线这里了解一下吧.想python学习课程?学习路线网免费下载海量python教程,上班族也能在家自学 ...

  9. 学习《零基础入门学习Python》电子书PDF+笔记+课后题及答案

    初学python入门建议学习<零基础入门学习Python>.适合新手入门,很简单很易懂.前一半将语法,后一半讲了实际的应用. Python3入门必备,小甲鱼手把手教授Python,包含电子 ...

  10. 学习《人人都是产品经理2.0:写给泛产品经理》高清中文PDF+苏杰(作者)

    <人人都是产品经理2.0--写给泛产品经理>将从人开始,以人结束,中间说事,以一个产品从无到有的过程为框架--想清楚.做出来.推出去,外加一章综合案例.其中,最重要的想清楚.做出来.推出去 ...

随机推荐

  1. java基础-IO流-day13

    目录 1. IO的概念 2. 一个一个字符 完成文件的复制 3. 字节流 4. 转换字节流 5. System.in 7.基本数据类型的数据 8. object的处理 1. IO的概念 计算机内存中的 ...

  2. zookeeper源码(06)ZooKeeperServer及子类

    ZooKeeperServer 实现了单机版zookeeper服务端功能,子类实现了更加丰富的分布式集群功能: ZooKeeperServer |-- QuorumZooKeeperServer |- ...

  3. 解决在Edge浏览器中使用不了(找不到)new bing的情况

    1.问题 我们有时候看不到下图圈出部分的信息,无法找到New Bing的入口(这边是空的) 2.解决方式 1.选择右上角的三条杠,并选择其中的settings 2.将其中的country一项改为外国即 ...

  4. C:\Keil_v5\ARM\ARMCC\include\stdint.h contains an incorrect path.

    1.问题 在使用Keil uvison5打开例程代码进行学习时,发现部分.h文件无法读取 2.解决方法 1.找到如图的设置按钮(小锤子) 2.根据自己所用的是C/C++还是ARM选择(我这里是C/C+ ...

  5. PolarD&N2023秋季个人挑战赛—Misc全解

    签个到叭 题目信息 压缩包带密码,放到010查看PK头错误,改回去.. 解压后得到 562+5Yiw5Lmf5LiN6IO96L+Z5LmI566A5Y2V5ZGA77yM5b+r5p2l55yL55 ...

  6. IDE-常用插件

    2021-8-25_IDE-常用插件 1. 背景 提升编写代码的舒适度,提升开发效率 2. 常用插件列表 IDE EVal Reset 白嫖付费的golang编辑器,reset插件可以重置golang ...

  7. [转帖]【sql server安全】sql server连接加密,sql server SSL加密连接

    https://www.cnblogs.com/gered/p/13595098.html#_label1_0 MSSQL - 最佳实践 - 使用SSL加密连接 回到顶部 author: 风移 回到顶 ...

  8. [转帖]UNIX SOCKET简介

    UNIX Domain SOCKET 是在Socket架构上发展起来的用于同一台主机的进程间通讯(IPC).它不需要经过网络协议栈,不需要打包拆包.计算校验和.维护序列号应答等.只是将应用层数据从一个 ...

  9. [转帖]Linux cut命令

    https://www.runoob.com/linux/linux-comm-cut.html#:~:text=Linux%20cut%E5%91%BD%E4%BB%A4%201%20-b%20%E ...

  10. 基于OpenJDK部署clickhouse-local镜像的快捷方法

    基于OpenJDK部署clickhouse-local镜像的快捷方法 摘要 前期搭建了一套基于OpenJDK的Clickhouse的服务端的镜像 可以简单使用dbeaver进行连接与使用. 后来发现需 ...