防抖节流从简单到复杂,一步一步从入门到深入了解(代码片段)

zhuangwei_8256 zhuangwei_8256     2023-02-16     275

关键词:

防抖

场景

 场景:
  一个搜索输入框,用户可通过实时输入调用接口返回用户想要的数据。

<input id="searchInput" type="text">
function handleInput(event) 
    let e = event || window.event;
    console.log('e', e.target.value);


document.getElementById("searchInput").oninput = handleInput;

 效果:

  很明显,每次输入都会调用一次,这种并不是我们想要的效果,试想,如果搜索框在用户实时输入时一直调用后台接口……想想都很可怕,接口调用太频繁,且如果调用的太频繁,可能存在网络等其他原因导致接口返回数据的先后顺序,那么可能前端回显到界面呈现给用户看的可能就不是用户想要的数据了。

  这时,防抖就应运而生了,
  什么是防抖?顾名思义,防抖,防抖,防止抖动,抖动这个动作是很频繁的,就类似于上面展示的这个实时输入的input事件,防止抖动,意思就是要防止太过频繁的抖动,减少抖动的次数。所以,什么是防抖?防抖就是让事件在规定时间内(一定的时间内)调用一次,直白的说就是减少调用次数,控制次数!

  实现原理:

  • 借助定时器 setTimeout,让事件在 setTimeout的时间内只调用一次,从而做到控制次数的目的;
  • 需要借助闭包,借助闭包的目的在于在实现防抖函数的时候,我们需要一个变量来保存setTimeout的返回值,进而通过这个返回值判断是否定时器内的函数还在调用。


  调用方法:

document.getElementById("searchInput").oninput = debounce(handleInput, 1000);


  先看效果:

  效果很明显:从gif 图中我们可以知道,只要当用户一直在输入内容时(也就是一直在调用 input 方法),是不会马上调用input方法的,是当用户停止输入后1秒(1000毫秒 = 1秒)才调用。这意味着只要用户在不停输入,定时器的返回值 timer 是会重新计算的,直白的说就是用户停止输入才开始计算这个时间,然后才调用 input 方法。

初版代码如下:

// 防抖:控制次数,在规定时间内只调用一次
// func 需要防抖的函数
// waitTime 定时器的时间,由这个时间控制调用次数
function debounce(func, waitTime) 
    // 借助闭包原理 定义一个变量记录 setTimeout的返回值
    let timer = null;
    return function(event) 
    	// 判断如果定时器方法正在运行,则清除定时器;
        if(timer) clearTimeout(timer); 
        // 使用timer 来接受定时器方法的返回值
        timer = setTimeout(() => 
            func(event); // 调用需要防抖的函数
        , waitTime);
    

 从初版代码我们可以很明显看出,参数需要写出来,这样如果参数很多呢,那我这个代码不是回显得很长?很别扭的感觉?

 这个时候,进阶版的代码来了。

 实现原理:
  利用 apply 方法(不建议使用 call 这个继承方法,call 方法是分别接受参数;而apply方法则是接受数组形式的参数。详见W3C中对这call方法以及apply方法的官方解释)。

进阶版代码如下

注:这个防抖方法容易和定时器版节流方法的代码逻辑搞乱,详细解释查看定时器版节流方法下面的定时器版节流与防抖方法代码逻辑详解说明!!!

箭头函数的写法:

// 防抖:控制次数,在规定时间内只调用一次
// func 需要防抖的函数
// waitTime 定时器的时间,由这个时间控制调用次数
function debounce(func, waitTime) 
    // 借助闭包原理 定义一个变量记录 setTimeout的返回值
    let timer = null;
    return function() 
        // 判断如果定时器方法正在运行,则清除定时器;
        if(timer) clearTimeout(timer); 
        // 使用timer 来接受定时器方法的返回值
        timer = setTimeout(() => 
        	timer = null;
            func.apply(this, arguments); // 调用需要防抖的函数
        , waitTime);
    

注:这里有一个注意点,在使用 apply 方法的时候,如果你的定时器是使用的箭头函数的写法,那么 apply 里面的参数可以直接写 this, arguments,如果你使用的是 setTimeout( function() , timeout) 的写法时,apply 的参数你就不能直接写 this, arguments,
原因是:

  • 1、this,这里涉及到一个this指向的问题,箭头函数的this 是指向离箭头函数最近的function,也就是调用function 方法的事件源,在示例中也就是input框了,而当你使用function 写定时器时,就直接改变了this的指向了,这时候的this就指向的时当前的定时器了;
  • 2、arguments,这里同样涉及到arguments 的使用问题,箭头函数是没有 arguments 这个参数集的,只有function 才会有 arguments 参数集。

所以,如果你用的是function 的写法来写定时器,你需要改造一下代码;

直接使用 function 函数的写法:

// 防抖:控制次数,在规定时间内只调用一次
// func 需要防抖的函数
// waitTime 定时器的时间,由这个时间控制调用次数
function debounce(func, waitTime) 
    // 借助闭包原理 定义一个变量记录 setTimeout的返回值
    let timer = null;
    return function() 
     	// 使用function 的写法来写定时器时,定义两个变量接收一下 this, arguments
     	let that = this;
     	let args = arguments;
     	
        // 判断如果定时器方法正在运行,则清除定时器;
        if(timer) clearTimeout(timer); 
        // 使用timer 来接受定时器方法的返回值
        timer = setTimeout( function() 
        	timer = null;
        	// 然后这里把参数换掉我们自己定义的参数;
            func.apply(that, args); // 调用需要防抖的函数
        , waitTime);
    

  到了这一步,有的朋友就在想,有的时候,定时器的时间如果设置的过长,加上如果调用后台接口时间比较久的话,用户在输入根本看不到效果,会给用户一种错觉:这个搜索框是不是没用?!
  这个时候,进阶版的防抖函数来了。
  既然可能存在这个情况,那么我们让用户在第一次调用的时候就立马执行一次函数,让用户先看到效果,让他知道这个功能是有用的。

 实现原理:
  增加一个参数,如果这个参数存在,那么就立刻调用。

终极版代码如下:

// 防抖:控制次数,在规定时间内只调用一次
// func 需要防抖的函数
// waitTime 定时器的时间,由这个时间控制调用次数
// immediate 是否需要立即执行,true 为立即调用,不传或者传false 不立即调用
function debounce(func, waitTime, immediate) 
    // 借助闭包原理 定义一个变量记录 setTimeout的返回值
    let timer = null;
    return function() 
        // 判断如果定时器方法正在运行,则清除定时器;
        if(timer) clearTimeout(timer); 
        if(!immediate)  // 如果这个参数不存在,还是之前的代码
            // 使用timer 来接受定时器方法的返回值
            timer = setTimeout(() => 
                func.apply(this, arguments); // 调用需要防抖的函数
            , waitTime);
         else  
            // 反之,如果存在,定义一个变量取反定时器的返回值,
            let isNow = !timer;
            // 当用户在不停的输入,调用时,这个timer 一直是有值的,意味着 isNow 就为fasle, 也就不会调用方法了
            // 当用户停止输入后的 waitTime 时间内,timer 的值就赋值为 null了,
            // 然后就会调用需要防抖的函数了
            timer = setTimeout(() => 
                // 在这里面将定时器的返回值赋值为 null,意为在一定时间后定时器不在运行中,
                // 然后停止输入一定时间后调用函数;
                timer = null;
                func.apply(this, arguments);
            , waitTime);
            // 如果这个变量判断为true,那么证明定时器不在运行,即立即执行
            if(isNow) func.apply(this, arguments); // 调用需要防抖的函数
        
    

 调用方法:

document.getElementById("searchInput").oninput = debounce(handleInput, 1000, true);

 效果如下:

  如图所示:当用户输入时,如果不停输入时不会调用,一定时间的首次输入时会立即调用一次,停止输入后在一定时间后会再次调用一次。

  我看有的写法是这样的
  这一步是没有加上的,如果这一步没有加上,意味着,一定时间内只有首次输入时会调用方法,也就是说用户不停输入后如果没有再次输入是不会调用方法的,只有再次输入后才会调用,如下图所示:

  这一步的话是否需要加上,看项目需求吧,大家择其一看情况而定即可。


节流

 接下来,我们来说下节流。
  什么是节流?节流,按照现在经常说的,节流开源,很直观的字面意义,就是节省“流量”,当然此“流量”非彼流量,在这里,节流为减少调用的频率,控制频率。与防抖不同,防抖是减少调用的次数,控制次数。

场景:

 场景:
  一个提交按钮,防止用户频繁点提交保存按钮。

<button id="submitBtn">提交</button>
function handleSubmit() 
    console.log("提交" + Date.now());


 节流一般有两种实现方式:两种使用的原理不一样

时间戳版节流

 实现原理:
  利用时间戳,记录上次调用时的时间和当前时间做对比,如果两者的时间差超过我们设置的时间,那么即可调用,反之不调用。

function throttle(func, waitTime) 
	// 定义一个变量记录调用方法时的时间
    let handleTime = 0;
    return function() 
    	// 获取当前时间戳
        let now = Date.now();
        // 判断 如果当前时间 - 调用时的时间 大于 我们设置的时间waitTime,
        // 直白的说就是看现在使用方法的时间减去上次调用方法的时间有没有超过我们设置的那个时间
        // 如果超过了就正常调用,如果没有超过,那么证明不符合我们设置的时间内,那么不调用。
        if (now - handleTime > waitTime) 
            func.apply(this, arguments);
            handleTime = now; // 记录调用时的时间,方便下次调用时知道上次调用的时间
        
    

  说明直接看代码注释即可,在此就不作冗余说明了。

定时器版节流

 实现原理:
  利用定时器,和防抖的实现原理类似,区别在于是否使用clearTimeout,详见代码下面的说明。

function throttle(func, waitTime) 
	// 定义一个变量保存定时器的返回值,然后通过这个变量判断定时器是否在运行中
    let timer;
    return function() 
    	// 这里定义两个变量保存this, arguments,意思和防抖的一样,
    	// 如果定时器使用function 的写法,就需要定义这两个变量;
    	// 如果使用箭头函数的写法这两个变量可以不用,
    	// 如果改变了写法记得修改调用节流函数时切换参数。
        let that = this;
        let args = arguments;
        // 判断如果定时器没有运行,则执行下面代码,如果定时器运行中不做任何操作
        if (!timer) 
            timer = setTimeout(() => 
            	// 在一定时间后waitTime,将定时器返回值赋值为null,即:让下次调用这些代码的条件符合;
                timer = null;
				// 这里我就不作参数的演示了,和初版防抖差不多,我就直接跳到进阶版了。
                func.apply(that, args) // 执行需要节流的函数
            , waitTime)
        
    

定时器版节流与防抖方法代码逻辑详解说明:

  • 定时器版节流的写法不需要判断 timer 存在的情况清除定时器。
  • 为什么?这是因为就算你清除了定时器,定时器的返回值还是有值,而且 clearTimeout 的作用是什么?是清除setTimeout 里面的代码块,点击查看详情
  • 为什么防抖那里可以加?这是因为防抖那里没有 if (!timer) 这层判断,没有这层判断的话,初次调用的时候是没有清除定时器的,那么定时器里面的代码是会执行的,再次调用的时候,虽然 timer有值,并执行了 clearTimeout(timer),但是这个是清除了上次的定时器里面的代码块,这次的定时器里面的代码块仍是会执行的,你不断频繁的调用,就会不停的清除上次的定时器,但是最后一次的定时器还是在运行,里面的代码块最终还是会执行,所以也就实现了防抖的功能。
  • 而这个定时器版节流代码的话,如果你加了 if(timer) clearTimeout(timer); 并且有这个判断 if (!timer) ,那么在初次调用时,会执行定时器里面的代码,但是如果你快速点击(频繁的调用)的话,这个timer 是有值的,就会执行 if(timer) clearTimeout(timer); 当你执行了 clearTimeout(timer),也就把初次的定时器里面的这两行代码 timer = null; func.apply(that, args) 清除了,也就是说这个timer 永远都是有值的,也就是永远都不会进入if (!timer) 这个判断分支了,不进入这个判断分支也就不会重置 timer和执行需要节流的函数了,这样失去了封装节流函数的意义了。
    当然,如果你在定义的时间后再调用,也就是说,比如我这边设置的是3秒,如果你在初次调用的3秒后再次调用,也是可以满足条件的,但是这种情况于项目需求应该就是不符合了,所以排除这个可能性。
  • 如果加了这个判断的代码解析如下

----------------------------------------错误分割线-------------------------------------------
以下是(反面示例)错误定时器版节流代码,注意注意,不要使用这个!!!

function throttle(func, waitTime) 
    let timer;
    return function() 
        let that = this;
        let args = arguments;
        // 如果你在这一步判断了当定时器返回值timer存在,然后你使用clearTimeout 清掉的话,
        // 意味着,这两行代码不会执行,
        // timer = null;
        // func.apply(that, args) 
        // 且 timer 仍然有值,那么在这个timer 有值的情况下, 
        // if (!timer) 这个判断分支就永远都不会进入,也就代表着永远不会执行需要节流的函数了。

		// 初次不会执行,第二次调用的时候执行了,并清除了上次定时器里面的代码块,
		// timer 没有重新赋值,
		// 所以这个timer 是一直有值的,那么这个 if (!timer) 判断分支就不会进入了。
        if(timer) clearTimeout(timer); 
        if (!timer) 
            timer = setTimeout(() => 
                timer = null;
                func.apply(that, args) // 执行需要节流的函数
            , waitTime)
        
    

以上是(反面示例)错误定时器版节流代码,注意注意,不要使用这个!!!
----------------------------------------错误分割线-------------------------------------------




 调用方法:

document.getElementById("submitBtn").onclick = throttle(handleSubmit, 3000);

 效果如下:

  
  

写在末尾

  文章略长,望大家多多见谅。

  如有问题,麻烦留言指出,谢谢。

ansible一步一步从入门到精通

一:安装ansiblemac:1.安装Homebrew(gettheinstallationcommandfromtheHomebrewwebsite).2.安装Python2.7.x(brewinstallpython).3.安装Ansible(sudopipinstallansible).linux:如果系统中安装了python-pip和python-devel,你可以使用pip安装ansib 查看详情

ansible一步一步从入门到精通

一:本地基础测试环境搭建使用vmware或者virtualbox创建一个linux虚拟机(我的是centos6.6),关闭iptables和selinux将上面的服务器地址加入上一篇bolg的hosts文件中exampegroup中同样配置ssh秘钥验证二:你的第一个playbook新建ntp.yml如下:---&nb... 查看详情

ansible一步一步从入门到精通上

一:一个简单的Playbookplaybook比起shell脚本的优势,是幂等性,值得是运行一次和多次的结果都是一样的,不会对系统有影响一个简单的playbook:  1 ---  2 - hosts:  all  3   tasks: ... 查看详情

从简单到复杂:深入了解javascript中的this绑定规则(代码片段)

前言大家好,我是CoderBin,在JavaScript中,this是一个非常重要的概念,属于进阶知识,不管是在面试还是日常开发中都是非常常用的。所以本次给大家总结了关于this的绑定规则,来帮助大家更加深入的掌握这个知识点。希望对大... 查看详情

深入了解c指针

...处起,根据运算符优先级结合,一步一步分析.下面让我们先从简单的类型开始慢慢分析吧:int p;  &nb 查看详情

libevent使用例子,从简单到复杂

     本文从简单到复杂,展示如何使用libevent。网上的许多例子都是只有服务器端的,本文里面客户端和服务器端都有,以飨读者。     关于libevent编程时的一些疑问可以阅读《libevent编程疑难... 查看详情

mybatis源码解析,一步一步从浅入深:映射代理类的获取

在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们提到了两个问题:  1,为什么在以前的代码流程中从来没有addMapper,而这里却有getMapper?  2,UserDao明明是我们定义的一个接口类,根本没有定义... 查看详情

libevent使用例子,从简单到复杂

...44/article/details/39670221       本文从简单到复杂,展示如何使用libevent。网上的许多例子都是只有服务器端的,本文里面客户端和服务器端都有,以飨读者。     关于lib 查看详情

awk从简单到复杂

1、语法和参数说明语法awk[options]'script'var=valuefile(s)或awk[options]-fscriptfilevar=valuefile(s)或awk[options]'patternaction'FILE....选项参数说明:-Ffsor--field-separatorfs指定输入文件折分隔符,fs是一个字符串或者是一个正则 查看详情

[z]libevent使用例子,从简单到复杂

...n.net/luotuo44/article/details/39670221     本文从简单到复杂,展示如何使用libevent。网上的许多例子都是只有服务器端的,本文里面客户端和服务器端都有,以飨读者。     关于libevent编程时的一些... 查看详情

css从简单到复杂的动态效果,你值得拥有

<html><head><style>.tip{ height:10px; padding-left:20%; background-color:#FFFFFF; text-align:center; line-height::10px; color:#dddcdc; font-size:15px;}p:active{ text-decoration:under 查看详情

一步一步学习jni(代码片段)

本文来自网易云社区作者:孙有军前言本篇的主要目的就是JNI开发入门,使大家对JNI开发流程有一个大致的了解,后续再进行深入学习。JNI不是Android特有的,JNI是JavaNativeInterface单词首字母的缩写,就是指用C或者C++开发的接口。... 查看详情

一步一步学习jni(代码片段)

本文来自网易云社区作者:孙有军前言本篇的主要目的就是JNI开发入门,使大家对JNI开发流程有一个大致的了解,后续再进行深入学习。JNI不是Android特有的,JNI是JavaNativeInterface单词首字母的缩写,就是指用C或者C++开发的接口。... 查看详情

c++网络编程——收发一个快递

...自己一些时间,梳理一下自己摸索网络开发的一些东西,从简单到复杂,一步一步的进阶。希望能让自己更进一步,也希望能帮助一些和我当年一样懵逼的同行们,坚持就是胜利! 下面来看一个简单的示例:server.cpp#i 查看详情

一步一步教你yaml快速入门

...和结构关系的时候,十分欠缺,而XML在数据格式描述和较复杂数据内容展示方面,更加优秀。到后面介绍JSON格式的时候,我们发现JSON格式比较XML格式,更加方便(除去数据格式限制之外),所以现在很多配置文件(比如Nginx和... 查看详情

从 React Native init 一步一步地 React Native Web

】从ReactNativeinit一步一步地ReactNativeWeb【英文标题】:Reactnativewebstepbystepfromreactnativeinit【发布时间】:2021-07-2304:59:04【问题描述】:谁能给我一步一步从一个新的reactnative项目安装reactnativeweb?1)初始化反应原生2)npminstallreact-domrea... 查看详情

lfu五种实现方式,从简单到复杂(代码片段)

...自己实现一遍总归是好的。因此,我就把五种求解方式,从简单到复杂,都讲一遍。LFU实现力扣原题描述如下:请你为最不经常使用(LFU)缓存算法设计并实现数据结构 查看详情

公子奇带你一步一步了解java8中lambda表达式

在上一篇《公子奇带你一步一步了解Java8中行为参数化》中,我们演示到最后将匿名实现简写为1(Policepolice)->"浙江".equals(police.getPoliceNativePlace());这是一个带有箭头的函数,这种写法在Java8中即为Lambda表达式。那么我们就来好好... 查看详情