防抖节流一步一步详细讲解,从简单到复杂,从入门到深入了解,再到 Vue 项目中是怎样调用防抖节流方法的

Posted zhuangwei_8256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了防抖节流一步一步详细讲解,从简单到复杂,从入门到深入了解,再到 Vue 项目中是怎样调用防抖节流方法的相关的知识,希望对你有一定的参考价值。

防抖

场景

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

<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);

 效果如下:

  
  

Vue 中是如何调用防抖和节流的

项目中通常是将防抖和节流方法封装到一个js文件中,然后在对应项目中引入并调用;

  1. 创建并封装防抖、节流的eventHandle.js文件

eventHandle.js


/**
 * 防抖
 * @param Object func 方法
 * @param Object wait 间隔时长 毫秒
 * @param Object immediate 是否事件触发时立即执行一次
 */
export function debounce(func, wait, immediate) 

    var timeout;

    return function () 
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) 
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function()
                timeout = null;
            , wait)
            if (callNow) func.apply(context, args)
        
        else 
            timeout = setTimeout(function()
                func.apply(context, args)
            , wait);
        
    


/**
 * 节流
 * @param function func 方法
 * @param number waitTime 间隔时长  毫秒
 */
function throttle(func, waitTime) 
	// 定义一个变量记录调用方法时的时间
    let handleTime = 0;
    return function() 
    	// 获取当前时间戳
        let now = Date.now();
        // 判断 如果当前时间 - 调用时的时间 大于 我们设置的时间waitTime,
        if (now - handleTime > waitTime) 
            func.apply(this, arguments);
            handleTime = now; // 记录调用时的时间,方便下次调用时知道上次调用的时间
        
    


export default 
	debounce,
	throttle


  1. 对应文件中引入并调用
import $eventHandle from '@/utils/eventHandle.js'

  

  1. 在methods 中调用

Vue 中调用防抖


// 防抖处理搜索框事件
handleInput: $eventHandle.debounce( function(e) 
	this.searchValue = e.target.value; // 获取模糊搜索值
	this.getList(); // 调用搜索列表的方法
, 1000),

Vue 中调用节流

// 节流处理提交订单按钮事件
// 一点击的就会调用,然后在5秒内不会再调用
handleSubmit: $eventHandle.throttle( function(e) 
	this.submitOrder(); // 调用提交订单的方法
, 5000),

  
  

写在末尾

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

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

以上是关于防抖节流一步一步详细讲解,从简单到复杂,从入门到深入了解,再到 Vue 项目中是怎样调用防抖节流方法的的主要内容,如果未能解决你的问题,请参考以下文章

防抖节流 从简单到复杂,一步一步从入门到深入了解

这样一步一步推导支持向量机,谁还看不懂?

跟老杜从零入门MyBatis到架构思维使用MyBatis完成CRUD- insert(Create)

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

[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能

LFU五种实现方式,从简单到复杂