前言

防抖函数和节流函数,无论是写业务的时候还是面试的时候,想必大家已经听过很多次了吧.但是大家在用到的时候,有了解过他们之间的区别嘛,他们是如何实现的呢?还是说只是简单的调用下像lodashunderscore这种第三方库提供给我们的节流和防抖函数呢?

本文接下来将会带你们了解下这两者的区别,以及我们该如何手写实现这两个函数.

防抖函数和节流函数的区别

防抖函数:是指触发了一个事件,在规定的时间内,如果没有第二次事件被触发,那么他就会执行.换句话讲,就是说,如果不断有事件被触发,那么规定的执行时间将会被不断推迟

节流函数:指的是在规定时间内,你无论触发多少次事件,你也只会执行一次.我举个生活中的例子,就很好理解了.王者荣耀这个游戏可能很多人多玩过,每个英雄都有自己的技能,在我们点击一次后,该技能会进入冷却时间,即使我们点的再快,该技能在冷却时间好之前也只能触发一次(我们第一次点击的时候)

防抖函数的实现

我将实现防抖函数的四个功能,希望大家能一步步的跟着来,循序渐进,相信大家一定会有收获的

基本实现

我们可以想下,要想一个事件在规定时间过后执行,在JS中该怎么实现

好 时间到

定时器,小伙伴们肯定都知道的吧

触发事件,在一定时间后执行,这个可以使用定时器解决了.

那么 接下来还有一个问题 在触发事件后,再触发事件,该如何让他推迟执行呢?

如果规定时间内,再触发的话,我们就把之前创建的定时器删除不就好了,对不对?

这样是不是就解决了我们的问题,好,我们现在来写下代码,怕大家有点不明白

function debounce(fn, delay) {
//定义一个定时器
let timer = null;
// 每次触发的时候 清空上一次的定时器
const _debounce = function () {
if (timer) clearTimeout(timer);
//根据传进来的延时 执行
timer = setTimeout(() => {
fn();
}, delay)
}
return _debounce;
}

这段代码还是比较容易的吧,相信小伙伴们肯定都懂了

但是这段代码还是有点问题,我们来调用下第三方库的underscore的防抖函数

<body>
<button>取消</button>
<input type="text">
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
const btn = document.querySelector("button");
const input = document.querySelector("input");
let count = 0;
function test(event) {
// 注意这里的this 和 event
console.log(`发送${++count}网络请求`, this, event);
return "我是返回结果";
}
input.oninput = _.debounce(test, 2000);
</script>
</body>

我们打开浏览器调试,看下输出结果

可以看到this和Event输出是没有任何问题的.

再来看看我们的输出

你会发现 this是window了 而Event是undefined.

这是为什么呢?

这是因为 我们写的代码没有对this进行一个绑定,同时也没有将DOM元素的event接收

fn()直接执行 这时候的this是直接指向window的

function debounce(fn, delay) {
let timer = null;
//使用剩余参数接收所有的参数 DOM在调用这个函数的时候,我们就能接收到event了
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
//注意 我们使用apply进行一个换绑,绑到执行这个的DOM元素上
fn.apply(this,args);
}, delay)
}
return _debounce;
}

至此,我们这个防抖函数的基本实现就没有任何问题了

看到这里的小伙伴们,你们真不错

这个基本实现 拿来应付面试已经够了,接下来我们还有一些额外的功能要实现,想看的可以继续往下看了,现在不想看的也可以收藏下,以后来看.

立即执行

在某些应用场景下,比如搜索的时候,你输入第一个字符的时候,他就会联想出一系列的字符,他不会等待一段时间后再去执行,而是会立马执行,我们接下来实现下这个功能

首先,立即执行这个功能,我们可以将它交给用户来决定是不是要使用这个功能

debounce(fn,delay,immediate=false)

我们以参数的形式传递,默认是关闭的

好,我们现在来看下代码实现

function debounce(fn, delay, immediate = false) {
let timer = null;
//代码规范 我们最好不要修改 用户传递进来的参数
//所以我们在下面声明了一个变量 用于控制
let isInvoke = false;
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
//如果immdiate为true
//isInvoke取反为true
if (immediate && !isInvoke) {
//会立马执行一次
fn.apply(this, args);
//同时将isInvoke设置为true,防止下次触发的时候 又再次触发立即执行
isInvoke = true;
} else {
//第一次触发结束立即执行后
//isInvoke会限定在定时器中 输入结束后 才会重新刷新isInvoke
timer = setTimeout(() => {
//剩下的操作在规定时间内 会等待定时器结束
fn.apply(this, args);
//同时重新刷新inInvoke
isInvoke = false;
}, delay)
}
}
return _debounce;
}

好,这一块还是比较简单的吧,相比大家应该看懂了,如果有什么不懂的地方,欢迎评论区留言,我看见了就会回答的

那么我们开始下一个篇章的 如果用户输入之后 不想让他请求呢 这时候我们就需要一个取消功能了,对,我们接下来就是要实现取消功能

取消功能

我们该如何在剩余的时间内取消请求呢?

对 没错! 清空定时器

我们只需要在我们返回的函数上加个静态方法 给用户提供个取消功能即可

我们来看下代码实现

 // 给返回的这个函数添加一个静态方法 用于取消请求
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
}

是不是很简单呢? 就这么简单一行代码 取消功能就完成了

好,我们还有最后一个功能需要实现的 那就是如果开发者想要拿到请求后的返回结果的话,我们现阶段的防抖函数能不能做到呢? 貌似不行吧?

所以接下来,我们来实现最后一个功能 取到返回结果

返回结果

我们思考一个问题 返回结果在哪呢?

用户传递一个函数给我们 返回一个新的函数

那么返回结果一定是在用户传递给我们的那个函数上的

所以关键就是 将用户的那个函数的返回结果传递出来

现在 我们这里有两个方案

  1. 回调函数
  2. Promise

我们先来看下回调函数的版本

// 回调函数版本
function debounce(fn, delay, immediate = false, resultCallBack) {
let timer = null;
let isInvoke = false;
let result = null;
const _debounce = function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !isInvoke) {
//接收结果
result = fn.apply(this, args);
resultCallBack(result);
isInvoke = true;
} else {
timer = setTimeout(() => {
//接收结果
result = fn.apply(this, args);
resultCallBack(result);
isInvoke = false;
}, delay)
}
}
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
isInvoke = false;
}
return _debounce;
}

实际应用

        const _debounce = () => {
debounce(test, 1000)().then(res => {
console.log(res);
})
}
input.oninput = _debounce;

回调函数的是不是比较简单? 我们来看下Promise版本的 在实际应用的时候要注意一些坑

function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoke = false;
let result = null;
const _debounce = function (...args) {
//在返回的函数中 直接整体返回一个Promsie对象
//将结果传入 resolve中
return new Promise((resolve, rejected) => {
if (timer) clearTimeout(timer);
if (immediate && !isInvoke) {
result = fn.apply(this, args);
resolve(result)
isInvoke = true;
} else {
timer = setTimeout(() => {
result = fn.apply(this, args);
resolve(result);
isInvoke = false;
}, delay)
}
})
}
_debounce.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
isInvoke = false;
}
return _debounce;
}

实际调用

  const _debounce = function(...args){
debounce(test, 1000).apply(this,args).then(res => {
console.log(res);
})
};
input.oninput = _debounce;

注意到了吧 我们对原来的函数又封装了一层 因为只有这样才能拿到promise的结果

同时this和event不会出问题

看到这里的小伙伴们真棒,相信你们防抖函数一定没问题了,待会我们就开始讲解 节流函数该如何实现

节流函数的实现

节流函数我们也是从几个方面逐步实现,带着大家一步步的解开节流函数的面纱.

基本实现

大家可以想下,节流函数该如何实现.

一段时间内,只会触发一次操作,后续的操作就不会被触发.

我们可以拿到当前的时间戳 来进行计算

我们直接通过代码来讲吧 比较方便讲

function throttle(fn, interval) {
let lastTime = 0;
const _throttle = function () {
//首先拿到当前的时间
const nowTime = new Date().getTime();
//传递进来的时间间隔 用当前的时间减去上一次触发的时间
//得到最新的剩余时间
const reamainTime = interval - (nowTime - lastTime);
if (reamainTime <= 0) {
fn();
//如果剩余时间小于0 说明已经达到一个间隔
//并且将现在的时间赋值给lastTime
//在时间间隔内 这样无论执行多少次 都只会执行第一次的操作
//因为第一次的lastTime是0 而nowTime是比较大的
//减去之后一定是个负数 所以会执行第一次
//而不会执行后续的操作
lastTime = nowTime;
}
}
return _throttle;
}

大家看下我上面这段代码 还是比较好理解的吧,面试的话能够写出这一部分已经很可以了,但是要想更加出彩,能和面试官多唠会的吧,我们接着看下面的实现

leading实现

我们在基本实现中,其实已经将这个功能已经实现了,但是并不是可以控制的,我们这个实现是将是否首次触发交给用户来决定,大家可以想下该如何实现

基本实现中,我们是如何实现第一次触发的?

是不是通过拿到的时间戳非常大,而lastTime为0所导致的呢?

所以我们是不是可以让lastTime也能拿到当前的时间戳呢,这样子, nowTime和lastTime相减的时候,是不是就不会变成负数呢?

代码实现

// 考虑到 我们后面会有很多功能要实现
//所以我们使用选项来进行配置.避免造成更多参数
function throttle(fn, interval, option = { leading: true }) {
let lastTime = 0;
const { leading } = option;
const _throttle = function () {
const nowTime = new Date().getTime();
//在 leading和lastTime为false的情况下
//就将nowTime赋值给lastTime,这样就不会在第一次就执行操作了
if (!leading && !lastTime) lastTime = nowTime;
const reamainTime = interval - (nowTime - lastTime);
if (reamainTime <= 0) {
fn();
lastTime = nowTime;
}
}
return _throttle;
}

大家是不是理解了呢? 我个人认为还是比较好懂的吧,不懂的可以在评论区留言,我看到就会给大家解答的

接下来,我们看下和这个情况相反的一种状况,如果我们想要在最后一次操作的时候进行一次触发操作的话,该如何去做呢?

trailing实现

这一块是比较难的部分了,会有点难,大家不懂的话,要多看几遍,实在有不明白的,欢迎评论区留言

首先最后一次触发操作时,我们该怎么样让它执行?

我提供一个思路,当我们最后一次触发操作的时候,拿到距离间隔还有多少时间结束,加上一个定时器,让他根据这个剩余时间去按时执行

代码实现

function throttle(fn, interval, option = { leading: true, tralling: false }) {
let lastTime = 0;
let timer = null;
const { leading, tralling } = option;
const _throttle = function (...args) {
const nowTime = new Date().getTime();
if (!leading && !lastTime) lastTime = nowTime;
const reamainTime = interval - (nowTime - lastTime);
if (reamainTime <= 0) {
fn.apply(this, args);
lastTime = nowTime;
if (timer) {
clearTimeout(timer)
timer = null;
}
// 如果执行了这一部分 那么后面的tralling就没有必要去执行
// 说明刚好执行到了这一步 后面的最后按下 就不需要
return;
}
if (tralling && !timer) {
timer = setTimeout(() => {
timer = null;
/** `
* 首先 按下第一次的时候 这个定时器已经被加上了
* 每次进来的时候 等待一定时间 定时器会被置空 方便下次使用
* 根据剩余时间 来判断执行
* 如果leading为false lastTime会被设置为0 会在规定的剩余时间到达后 去执行这个函数 而remianTime那个部分就不会被执行 因为remainTime会一直保持在一个正数状态
* 如果leading为true lastTime会被设置为当前的时间 这样在下一次的操作下,remainTime才会发生变化
*
*/
lastTime = !leading ? 0 : new Date().getTime();
fn.apply(this, args);
}, reamainTime)
}
}
return _throttle;
}

是不是比较难懂呢? 我在来解释一下啊

  • 首先如果remainTime已经小于0了,那么fn就会去执行,我们也就不需要去执行后续的操作了 会直接返回
  • 那么如果remainTime没有小于0,我们会设置定时器,在定时器内部,我们需要先把timer清空,防止下一次触发的时候又触发了.
  • 其次,我们要将lastTime进行一个处理
    • 如果我们之前设置的leading是false的话,那么我们需要将lastTime置为0,这样在下一次的触发操作的时候,才能触发leading为false的情况下的逻辑语句
    • leading为true的情况下,需要将lastTime设置为当前的时间戳,这样在下一次的操作的时候,才会remainTime才会发生变化,逻辑才能执行下去.

大家有没有听明白呢? 可能是会有点难懂,但是好好多看几遍,还是能够理解的我相信!!!

接下来的操作就比较简单了,大家可以安心食用,和防抖函数一样,是取消功能和返回结果

取消功能和返回结果

因为这个和防抖函数是一样的,所以我这里直接就放代码了

function throttle(fn, interval, option = { leading: true, tralling: false, resultCallback }) {
let lastTime = 0;
let timer = null;
let result = null;
const { leading, tralling, resultCallback } = option; // 两种结果回调
//和防抖函数是一样的
//1. 通过传递一个回调函数
//2. 通过promise 进行结果回调
const _throttle = function (...args) {
return new Promise((resolve, reject) => {
const nowTime = new Date().getTime();
if (!leading && !lastTime) lastTime = nowTime;
const reamainTime = interval - (nowTime - lastTime);
if (reamainTime <= 0) {
result = fn.apply(this, args);
resultCallback(result);
resolve(result);
lastTime = nowTime;
if (timer) {
clearTimeout(timer)
timer = null;
}
return;
}
if (tralling && !timer) {
timer = setTimeout(() => {
timer = null;
lastTime = !leading ? 0 : new Date().getTime();
result = fn.apply(this, args);
resultCallback(result);
resolve(result);
}, reamainTime)
}
})
}
//取消功能
_throttle.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
lastTime = 0;
}
return _throttle;
}

大家可以看下是不是一摸一样的呢? 非常轻松吧

能看到这里的小伙伴们,一定收获很大,你们真棒


最后,让我介绍一下我自己,我是CodeSpirit,一名正在学习前端的coder,关注我,带你学习更多前端知识

[手写系列] Spirit带你实现防抖函数和节流函数的更多相关文章

  1. 手写系列:call、apply、bind、函数柯里化

    少废话,show my code call 原理都在注释里了 // 不覆盖原生call方法,起个别名叫myCall,接收this上下文context和参数params Function.prototy ...

  2. 手写系列-实现一个铂金段位的 React

    一.前言 本文基于 https://pomb.us/build-your-own-react/ 实现简单版 React. 本文学习思路来自 卡颂-b站-React源码,你在第几层. 模拟的版本为 Re ...

  3. codevs1281 矩阵乘法 快速幂 !!!手写乘法取模!!! 练习struct的构造函数和成员函数

    对于这道题目以及我的快速幂以及我的一节半晚自习我表示无力吐槽,, 首先矩阵乘法和快速幂没必要太多说吧,,嗯没必要,,我相信没必要,,实在做不出来写两个矩阵手推一下也就能理解矩阵的顺序了,要格外注意一些 ...

  4. JS奇淫巧技:防抖函数与节流函数

    应用场景 实际工作中,我们经常性的会通过监听某些事件完成对应的需求,比如: 通过监听 scroll 事件,检测滚动位置,根据滚动位置显示返回顶部按钮 通过监听 resize 事件,对某些自适应页面调整 ...

  5. JS 防抖函数和节流函数

    文章转载自:木上有水 什么是防抖?什么是节流? 工作中我们经常会用一些方法监听某些事件的完成,比如scroll.resize.keyup等. 常规事件触发的时候,比如scroll,会在短时间内触发多次 ...

  6. js实现防抖函数和节流函数

    防抖函数(debounce) 含义:防抖函数指的是在特定的时间内没有再次触发,才得以进行接下来的函数运行: 用途:当window.onresize不断的调整大小的时候,为了避免不断的重排与重绘,可以用 ...

  7. [手写系列] 带你实现一个简单的Promise

    简介 学习之前 需要先对Promise有个基本了解哦,这里都默认大家都是比较熟悉Promise的 本次将带小伙伴们实现Promise的基本功能 Promise的基本骨架 Promise的then Pr ...

  8. 手写Struts,带你深入源码中心解析

    个人剖析,不喜勿喷 扫码关注公众号,不定期更新干活 在此申明本博文并非原创,原文:http://blog.csdn.net/lenotang/article/details/3336623,本文章是在 ...

  9. 手写简易SpringMVC

    手写简易SpringMVC 手写系列框架代码基于普通Maven构建,因此在手写SpringMVC的过程中,需要手动的集成Tomcat容器 必备知识: Servlet相关理解和使用,Maven,Java ...

随机推荐

  1. Matplotlib (一)

    Matplotlib 用于 创建出版质量图标的绘图工具库 目的是为python构建一个 Matlab 式的绘图接口 import matplotlib.pyplot as plt pyplot 模块包 ...

  2. 最大连续子数组和--dp

    最大连续子数组和 问题: 给定n个整数(可能为负数)组成的序列a[1],a[2],a[3],-,a[n],求该序列如a[i]+a[i+1]+-+a[j]的子段和的最大值.当所给的整数均为负数时定义子段 ...

  3. 第三次SQLServer试验解答

    1 --讲解函数: SUM.AVG.COUNT.MAX.MIN .GETDATE()等 2 --查询BookInfo表中购进价格最高的图书的信息 3 --查询BookInfo表中书的平均购进价格 4 ...

  4. 【TcaplusDB知识库】如何部署TcaplusDB Local 版

    [TcaplusDB知识库]部署TcaplusDB Local 版的准备操作 1. 版本介绍 TcaplusDB Local版,是为用户提供的一个满足本地开发调试的版本(基于Docker部署的可下载版 ...

  5. myeclipse maven web打包

    1.在当前的项目pom.xml的文件上,如下图所示:鼠标右键->run As->Maven Build...

  6. java8两个字段进行排序问题

    //这个解决问题 Comparator<Anjianxinxi> getLianriqi = Comparator.comparing(Anjianxinxi::getLianriqi). ...

  7. JavaMail发送邮件(超详细)

    一:邮件发送的基本概念 本文我将阐述使用JavaMail方式发送和接收Email的详细说明,本博客本着以后遇到类似的邮件发送需求可以直接把代码粘过去直接使用,快捷方便省时间,对于刚接触的JavaMai ...

  8. 在Ubuntu上安装Docker Engine

    在Ubuntu上安装Docker Engine 这篇文章是介绍如何在在Ubuntu上安装Docker Engine,就是Google翻译官方文档的版本,英语好的直接官方原文.原文 要在Ubuntu上开 ...

  9. 【原】MDC日志链路设计

    背景 我们项目中现有日志系统,采用的是slf4j+logback这套日志组件,也是Java生态里面比较常用的一个日志组件,但是随着分布式的演进,这套组件明显存在以下几个问题: 1.各种无关日志穿行其中 ...

  10. 文件/目录对比:diff命令

    命令格式 diff [参数] [文件1或目录1] [文件2或目录2] Linux diff命令用于比较文件的差异. diff以逐行的方式,比较文本文件的异同处. 如果指定要比较目录,diff会比较目录 ...