手写Spirit防抖函数underscore和节流函数lodash


Posted in Javascript onMarch 22, 2022

前言

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

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

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

手写Spirit防抖函数underscore和节流函数lodash

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

手写Spirit防抖函数underscore和节流函数lodash

防抖函数的实现

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

基本实现

我们可以想下,要想一个事件在规定时间过后执行,在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>

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

手写Spirit防抖函数underscore和节流函数lodash

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

再来看看我们的输出

手写Spirit防抖函数underscore和节流函数lodash

你会发现 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);
    }

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

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

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

返回结果

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

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

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

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

现在 我们这里有两个方案

  • 回调函数
  • 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;
}

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

以上就是手写Spirit防抖函数underscore和节流函数lodash的详细内容,更多关于pirit防抖函数underscore节流函数lodash的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
用 javascript 实现的点击复制代码
Mar 24 Javascript
js 获取浏览器高度和宽度值(多浏览器)
Sep 02 Javascript
将string解析为json的几种方式小结
Nov 11 Javascript
深入理解JavaScript系列(12) 变量对象(Variable Object)
Jan 16 Javascript
js的window.showModalDialog及window.open用法实例分析
Jan 29 Javascript
非常酷炫的Bootstrap图片轮播动画
May 27 Javascript
js微信支付实现代码
Dec 22 Javascript
wx-charts 微信小程序图表插件的具体使用
Aug 18 Javascript
H5实现手机拍照和选择上传功能
Dec 18 Javascript
微信小程序自定义菜单切换栏tabbar组件代码实例
Dec 30 Javascript
实用的 vue tags 创建缓存导航的过程实现
Dec 03 Vue.js
微信小程序中使用vant框架的具体步骤
Feb 18 Javascript
深入讲解Vue中父子组件通信与事件触发
Mar 22 #Vue.js
关于Vue中的options选项
Mar 22 #Vue.js
vue+echarts实现多条折线图
vue使用echarts实现折线图
浅谈Vue的computed计算属性
JavaScript实现外溢动态爱心的效果的示例代码
Mar 21 #Javascript
实现AJAX异步调用和局部刷新的基本步骤
Mar 17 #Javascript
You might like
简单采集了yahoo的一些数据
2007/02/14 PHP
探讨如何在php168_cms中提取验证码
2013/06/08 PHP
php抓取网站图片并保存的实现方法
2015/10/29 PHP
thinkphp3.x中display方法及show方法的用法实例
2016/05/19 PHP
利用Homestead快速运行一个Laravel项目的方法详解
2017/11/14 PHP
PHP的curl函数的用法总结
2019/02/14 PHP
PHP中类与对象功能、用法实例解读
2020/03/27 PHP
Javascript 代码也可以变得优美的实现方法
2009/06/22 Javascript
JS判断是否为数字,是否为整数,是否为浮点数的代码
2010/04/24 Javascript
jquery加载页面的方法(页面加载完成就执行)
2011/06/21 Javascript
JS实现简单的Canvas画图实例
2013/07/04 Javascript
jQuery队列操作方法实例
2014/06/11 Javascript
详解微信小程序开发—你期待的分享功能来了,微信小程序序新增5大功能
2016/12/23 Javascript
Angular JS 生成动态二维码的方法
2017/02/23 Javascript
BootStrap Table前台和后台分页对JSON格式的要求
2017/06/28 Javascript
vue.js select下拉框绑定和取值方法
2018/03/03 Javascript
微信小程序将字符串生成二维码图片的操作方法
2018/07/17 Javascript
axios的拦截请求与响应方法
2018/08/11 Javascript
JQuery中queue方法用法示例
2019/01/31 jQuery
JavaScript监听触摸事件代码实例
2019/12/30 Javascript
[01:04:08]完美世界DOTA2联赛PWL S3 INK ICE vs GXR 第一场 12.16
2020/12/18 DOTA
Python2.7基于淘宝接口获取IP地址所在地理位置的方法【测试可用】
2017/06/07 Python
Python排序搜索基本算法之希尔排序实例分析
2017/12/09 Python
Python Flask前后端Ajax交互的方法示例
2018/07/31 Python
python基础知识(一)变量与简单数据类型详解
2019/04/17 Python
Pycharm github配置实现过程图解
2020/10/13 Python
Django Auth用户认证组件实现代码
2020/10/13 Python
意大利一家专营包包和配饰的网上商店:Borse Last Minute
2019/08/26 全球购物
平面设计自荐信
2013/10/07 职场文书
会计专业自荐信范文
2013/12/02 职场文书
通信研究生自荐信
2014/02/01 职场文书
大课间体育活动方案
2014/03/12 职场文书
Python中for后接else的语法使用
2021/05/18 Python
HTML+CSS实现导航条下拉菜单的示例代码
2021/08/02 HTML / CSS
Java spring定时任务详解
2021/10/05 Java/Android
基于Python实现西西成语接龙小助手
2022/08/05 Golang