手写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高级程序设计 XML、Ajax 学习笔记
Sep 10 Javascript
通过JS获取用户本地图片路径并显示的代码
Feb 16 Javascript
jQuery contains过滤器实现精确匹配使用方法
Apr 12 Javascript
JavaScript中的继承方式详解
Feb 11 Javascript
jQuery实现contains方法不区分大小写的方法
Feb 13 Javascript
JavaScript的Backbone.js框架环境搭建及Hellow world示例
May 07 Javascript
Highcharts 多个Y轴动态刷新数据的实现代码
May 28 Javascript
node.js基于fs模块对系统文件及目录进行读写操作的方法详解
Nov 10 Javascript
JS运动特效之同时运动实现方法分析
Jan 24 Javascript
vue的传参方式汇总和router使用技巧
May 22 Javascript
JS实现的获取银行卡号归属地及银行卡类型操作示例
Jan 08 Javascript
谈一谈vue请求数据放在created好还是mounted里好
Jul 27 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
Laravel框架运行出错提示RuntimeException No application encryption key has been specified.解决方法
2019/04/02 PHP
wordpress之js库集合研究介绍
2007/08/17 Javascript
JavaScript 学习笔记(十一)
2010/01/19 Javascript
javascript浏览器兼容教程之事件处理
2014/06/09 Javascript
PHP中使用微秒计算脚本执行时间例子
2014/11/19 Javascript
jquery+ajax实现跨域请求的方法
2015/01/20 Javascript
解决node-webkit 不支持html5播放mp4视频的方法
2015/03/11 Javascript
javascript笛卡尔积算法实现方法
2015/04/08 Javascript
JavaScript 事件入门知识
2015/04/13 Javascript
jquery实现鼠标悬浮停止轮播特效
2020/08/20 Javascript
Bootstrap项目实战之子栏目资讯内容
2016/04/25 Javascript
浅谈javascript中的事件冒泡和事件捕获
2016/12/28 Javascript
BootStrap模态框不垂直居中的解决方法
2017/10/19 Javascript
vue中img src 动态加载本地json的图片路径写法
2019/04/25 Javascript
jquery多级树形下拉菜单的实例代码
2019/07/09 jQuery
[02:17]2016国际邀请赛中国区预选赛VG战队领队采访
2016/06/26 DOTA
详解python中requirements.txt的一切
2017/03/03 Python
python入门教程之识别验证码
2017/03/04 Python
numpy自动生成数组详解
2017/12/15 Python
Python 实现一行输入多个值的方法
2018/04/21 Python
使用Template格式化Python字符串的方法
2019/01/22 Python
详解pandas的外部数据导入与常用方法
2019/05/01 Python
基于Python的Post请求数据爬取的方法详解
2019/06/14 Python
通过python扫描二维码/条形码并打印数据
2019/11/14 Python
Python3实现mysql连接和数据框的形成(实例代码)
2020/01/17 Python
Pandas时间序列:时期(period)及其算术运算详解
2020/02/25 Python
CSS3实现水平居中、垂直居中、水平垂直居中的实例代码
2020/02/27 HTML / CSS
css3实现简单的白云飘动背景特效
2020/10/28 HTML / CSS
利用HTML5 Canvas API绘制矩形的超级攻略
2016/03/21 HTML / CSS
MAC Cosmetics巴西官方网站:M·A·C彩妆
2019/04/18 全球购物
英国最好的包装供应商:Priory Direct
2019/12/17 全球购物
Java中实现多态的机制
2015/08/09 面试题
投标诚信承诺书
2014/05/26 职场文书
北京申奥口号
2014/06/19 职场文书
2015年“7.11”世界人口日宣传活动方案
2015/05/06 职场文书
PostgreSQL基于pgrouting的路径规划处理方法
2022/04/18 PostgreSQL