基础前端

debounce and throttle

2020-06-27  本文已影响0人  CondorHero

前言: 整个六月份就水了四篇文章,这完美的证明了我六月份基本没学啥东西 😤。这周在工作中主要是写了消息系统模块,自己封装的,测试老发现 bug ,只能发现一个修一个,还好是小姐姐,不然都要过来揍我了,新项目可的仔细点,多看 PRD 多测试。

主题:工作中我小伙伴喜欢用 lodash 这个库,我就喜欢 ES6 ,因为在我认识中,ES6 处理数据已经挺完美了,而且一个高手还能把处理数据的代码全给压平,代码结构由立体变扁平,类似这种:

//本人项目截取片段,写的不是很好多包涵
let unReadHeadIdArr = res.data.messages.map(item=>item && item.head.id);
let allMessArrId = window.sessionStorage.getItem("allMessVisable");
allMessArrId = allMessArrId ? JSON.parse(allMessArrId) : [];
let unReadHeadId = unReadHeadIdArr.filter(item=>!allMessArrId.includes(item));

其实我也是不想看 lodash,immediate这种第三方库,那么多 API,emmm,但是 ES6 也不是完美无缺的,其中就少了一个很重的东西,防抖和节流函数。而恰恰这两个函数在项目中很常用到。这时候就能体验到 lodash 的便捷了,虽然它的代码体积很大。最近我在写登陆模块的按钮的时候就体会很深,想起我以前技术差的时候能把项目模块功能写出来就很 happy 了,现在开始有点精力去注意这些边边角角了,that's brilliant。本着学习的态度,当然的研究下 debounce and throttle 的源码了,因为如果不使用 lodash 第三方库的时候,我们可以在项目中 util 文件夹中,放一些项目工具函数的地方,引入这两个函数,然后愉快的使用。

underscorelodashjs 这两个库都有 debounce and throttle 用法都差不多,就以 underscore 为例进行研究吧。

先推荐一个视频教程看看思路:手写函数防抖和节流——小马哥_老师
还有一个文字版的教程也可以看看:underscore 函数去抖的实现

在去官网看看 debounce 和 throttle 的用法:

我了解的防抖和节流:

这是个 underscore CDN 可以打开对照源码阅读:underscore

一、基本骨架

鼠标移动无限次触发

鼠标移动无限次触发计数显示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
        div{
            height: 300px;
            width: 900px;
            margin: 50px auto;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #fff;
            font-size: 38px;
            background-color: #222;
        }
    </style>
</head>
<body>
    <div id="app"></div>
    <script>
        let count = 0;
        app.onmousemove = function(){
            this.innerHTML = count++;
        }
    </script>
</body>
</html>

二、debounce 实现

underscore 中 debounce 函数有三个参数:debounce(需要防抖的函数,间隔时间,执行顺序)

首先讲下实现的三个难点:

一版:能防抖、能绑定 this 和 event

<script>
    let count = 0;
    // debounce是一个高阶函数
    function debounce(func,wait){
        let timeout,context;
        return function(...args){
            // 这个函数里面的this就是要防抖函数要的this
            //args就是事件对象event
            context = this;

            // 一直触发一直清除上一个打开的延时器
            if(timeout) clearTimeout(timeout);
            // 停止触发,只有最后一个延时器被保留
            timeout = setTimeout(function(){
                timeout = null;
                // func绑定this和事件对象event,还差一个函数返回值
                func.apply(context,args);
            },wait);
        }
    }
    function wirteCount(){
        this.innerHTML = count++;
    }
    // debounce被执行必须返回一个函数
    app.onmousemove = debounce(wirteCount,1000);
</script>

二版:增加函数返回值
如果需要防抖的函数 wirteCount 函数有返回值我们也应该予以保留。

function wirteCount(){
    this.innerHTML = count++;
    return "我需要返回一些东西";
}

二版实现,就增加了一个 result 变量来接收 wirteCount 函数返回值:

<script>
    let count = 0;
    // debounce是一个高阶函数
    function debounce(func,wait){
        let timeout,context,result;
        return function(...args){
            // 这个函数里面的this就是要防抖函数要的this
            //args就是事件对象event
            context = this;

            // 一直触发一直清除上一个打开的延时器
            if(timeout) clearTimeout(timeout);
            // 停止触发,只有最后一个延时器被保留
            timeout = setTimeout(function(){
                timeout = null;
                // func绑定this和事件对象event,还差一个函数返回值
                result = func.apply(context,args);
            },wait);

            return result;
        }
    }
    function wirteCount(){
        this.innerHTML = count++;
        return "我需要返回一些东西";
    }
    // debounce被执行必须返回一个函数
    app.onmousemove = debounce(wirteCount,1000);
</script>

最最难的点来了,debounce 第三个参数的实现,定义防抖函数刚触发就执行,还是触发之后等 wait 秒在执行。


三版:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
        section{
            margin: 50px auto;
            width: 900px;
            height: 300px;
        }
        div{
            height: 300px;
            width: 400px;
            float: left;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #fff;
            margin-left: 50px;
            font-size: 38px;
            background-color: #222;
        }

    </style>
</head>
<body>
    <section>
        <div id="app"></div>
        <div id="box"></div>
    </section>
    
<script>
    let count = 0;
    let idx = 0;
    // debounce是一个高阶函数
    function debounce(func,wait,immediate){
        let timeout,context,result;
        return function(...args){
            // 这个函数里面的this就是要防抖函数要的this
            //args就是事件对象event
            context = this;

            // 一直触发一直清除上一个打开的延时器
            if(timeout) clearTimeout(timeout);

            if(immediate){
                // 第一次触发,timeout===undefined恰好可以利用timeout的值
                const callNow = !timeout;
                timeout = setTimeout(function(){
                    timeout = null;
                },wait);
                if(callNow) result = func.apply(context,args);

            }else{
                // 停止触发,只有最后一个延时器被保留
                timeout = setTimeout(function(){
                    timeout = null;
                    // func绑定this和事件对象event,还差一个函数返回值
                    result = func.apply(context,args);
                },wait);
            }
            

            return result;
        }
    }
    function wirteCount(){
        if(this.id === "box"){
            this.innerHTML = count++;
        }else{
            this.innerHTML = idx++;
        }
        return "我需要返回一些东西";
    }
    // debounce被执行必须返回一个函数
    app.onmousemove = debounce(wirteCount,500,false);
    box.onmousemove = debounce(wirteCount,500,true);
</script>
</body>
</html>

四版:现在就差一个取消操作了,取消操作我们需要做些改变,需要把 debounce 函数返回的函数提取出来进行扩展。


2S内可以取消事件执行
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    // debounce是一个高阶函数
    function debounce(func, wait, immediate) {
        let timeout, context, result;

        function resDebounced(...args) {
            // 这个函数里面的this就是要防抖函数要的this
            //args就是事件对象event
            context = this;

            // 一直触发一直清除上一个打开的延时器
            if (timeout) clearTimeout(timeout);

            if (immediate) {
                // 第一次触发,timeout===undefined恰好可以利用timeout的值
                const callNow = !timeout;
                timeout = setTimeout(function() {
                    timeout = null;
                }, wait);
                if (callNow) result = func.apply(context, args);

            } else {
                // 停止触发,只有最后一个延时器被保留
                timeout = setTimeout(function() {
                    timeout = null;
                    // func绑定this和事件对象event,还差一个函数返回值
                    result = func.apply(context, args);
                }, wait);
            }
            return result;
        }
        resDebounced.cancal = function(){
            clearTimeout(timeout);
            timeout = null;
        }
        return resDebounced;
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些东西";
    }

    const implement = debounce(wirteCount, 2000, false);

    // debounce被执行必须返回一个函数
    app.onmousemove = implement;

    // 取消防抖
    btn.onclick = implement.cancal;
    </script>
</body>

</html>

debounce 到此就写完了,到此你能看懂几乎所有第三方源码实现了,因为它们的实现基本都大同小异。

三、throttle 实现

你一定认为 debounce 都实现了,throttle 就不是很难了,No no 。throttle 最难的是第三个参数的实现思路,先来看看 underscore 中 throttle 的用法,throttle 前两个参数和 debounce 没啥区别,区别在于第三个参数不是 boolean 值,而是一个对象_throttle(func,wait,{leading: true,trailing:true}) leading 表示事件触发立即执行 func ,trailing 表示最后离开是否触发 func。两个都默认为 true。

前置知识:
debounce 函数一样,也有三个难点:

现在这个就比较简单了,通过这三个变量 let ctx, args, result; 完美接受实现,下面主要关注实现 throttle 第三个参数的实现。

3.1 leading 实现

leading :函数一触发就立即执行 func ,然后稳定的间隔执行 func ,最后一次离开不执行 func。

下面通过时间戳来实现的:

  1. 刚开始 old = 0 条件 now - old > wait 一定为真,也就是 func 立即触发。
  2. now - old > wait 第一次为真之后,func 就能稳定执行。
  3. 最后离开不会执行 func ,快速进入快速离开,你会发现 func 只在进入执行了一次。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    // throttle是一个高阶函数
    function throttle(func,wait){
        let ctx,args,result;
        let old = 0;
        return function(){
            ctx = this;
            args = arguments;
            let now = Date.now();
            if(now - old > wait){
                result = func.apply(ctx,args);
                old = now;
            };

            return result;
        }
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些东西";
    }

    const implement = throttle(wirteCount, 1000);

    // throttle被执行必须返回一个函数
    app.onmousemove = implement;

    </script>
</body>

</html>

3.2 trailing 的实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    function throttle(func,wait){
        let ctx,args,result,timeout;
        return function(){
            ctx = this;
            args = arguments;
            if(!timeout){
                timeout = setTimeout(function(){
                    timeout = null;
                    result = func.apply(ctx,args);
                },wait);
            };
            return result;
        }
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些东西";
    }

    const implement = throttle(wirteCount, 1000);

    // throttle被执行必须返回一个函数
    app.onmousemove = implement;

    </script>
</body>

</html>

3.3 leading 和 trailing 二合一实现 throttle

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>debounce and throttle</title>
    <style>
    div {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #fff;
        font-size: 38px;
        background-color: #222;
    }
    button{
        display: block;
        height: 30px;
        width: 60px;
        margin: 0 auto;
    }
    </style>
</head>

<body>
    <div id="app"></div>
    <button id="btn">取消</button>
    <script>
    let count = 0;
    let idx = 0;
    function throttle(func,wait,options = {}){
        let ctx, args, result, timeout, old = 0;
        let later = function(){
            result = func.apply(ctx,args);
            // 只要执行func,old时间戳就的重置
            old = Date.now();
            timeout = null;
        }

        function resThrottle(){
            ctx = this;
            args = arguments;
            let now = Date.now();

            // 第一次触发函数是否执行
            if(options.leading === false && !old){
                old = now;
            }
            if(now - old > wait){
                // 当条件now - old > wait为假时,会开启延时器
                // 所以我们要清除下
                if(timeout){
                    clearTimeout(timeout);
                    timeout = null;
                }
                result = func.apply(ctx,args);
                old = now;
            }else if(!timeout && options.trailing !== false){
                timeout = setTimeout(later,wait);
            };

            return result;
        }

        resThrottle.cancal = function(){
            clearTimeout(timeout);
            old = 0;
            timeout = context = args = null;
        };

        return resThrottle;
    }

    function wirteCount() {
        this.innerHTML = count++;
        return "我需要返回一些东西";
    }

    const implement = throttle(wirteCount, 5000);

    // throttle被执行必须返回一个函数
    app.onmousemove = implement;

    // 
    btn.onclick = implement.cancal;
    </script>
</body>

</html>

写作于北京昌平区 当前时间 Saturday, June 27, 2020 02:29:33

上一篇 下一篇

猜你喜欢

热点阅读