前端防抖与节流超详细讲解

您所在的位置:网站首页 防抖节流js 前端防抖与节流超详细讲解

前端防抖与节流超详细讲解

2023-11-11 09:17| 来源: 网络整理| 查看: 265

前言

防抖与节流通常作为项目优化的手段,一般都是为了防止用户在短时间内快而频地多次操作,触发动作执行。比如防止用户点击多次提交按钮,触发表单多次提交;防止用户拉动滚动条,多次触发加载更多等情况

防抖什么是防抖

防抖,顾名思义,就是防止抖动,简而言之就是多次快速频繁地触发事件,也只会执行一次事件函数,但是要记住,需要加上一个时间限制(总不能上一次触发和下一次触发前后相隔半个钟,也才执行一次吧?这样就不是防抖了,而是bug了)。

所以,防抖最正确的解释应该是,为了防止快速且频繁的触发事件而导致多次执行事件函数,我们这多次触发的事件只执行一次事件函数。而对于如何定义“频繁”,我们可以设定一个间隔时间,如果两次事件触发的间隔时间低于设定的时间,则定义为频繁。这样的场景有很多,比如监听滚动、鼠标移动事件onmousemove、频繁点击表单的提交按钮等等

debounce的特点是当事件快速连续不断触发时,动作只会执行一次。分为两种,一种是延迟debounce,一种是前缘debounce。 延迟debounce,是在周期结束时执行,前缘debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的间隔时间时,动作就会有多次执行。

延迟debounce策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。

image.png

前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。

image.png

实现防抖函数

版本1: 周期内有新事件触发,就清除旧定时器,重置新定时器;这种方法,需要高频的创建定时器。

此方法属于延迟debounce,即周期结束时执行,而且这种方法是多次创建新的计时器去替换旧的计时器,意思就是周期会不断刷新替换,只在最后一次周期结束后,才会执行

import React from 'react' export default function DebunceTest() { // 创建debounce防抖函数 const debounce = (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为防抖的周期时长 let timeout: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例 let context: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。 // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的this let args: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。 // 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。 // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数) // 通过定时器延迟执行事件函数 let run = ()=>{ timeout= setTimeout(()=>{ // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即防抖函数return出来的函数)的参数,之后执行func func.apply(context,args); }, wait); } // 清除定时器 let clean = () => { clearTimeout(timeout); } return function (this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错 context = this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。 // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。 // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。 args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。 // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。 // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了 if (timeout) { // 说明当前仍然处在上一个计时周期过程中,并且又触发了相同事件。则取消上一个计时周期,重新创建周期,覆盖上一次的操作 console.log('reset'); clean(); // 取消当前计时周期 run(); // 重新创建周期,覆盖上一次的操作 } else { // 说明上一个计时周期已经结束,那么可以开启新的周期 console.log('set'); run(); } } } // 要执行的事件函数 const handleClick = (e: any) => { console.log('提交表单', e); } return ( debounce(handleClick, 3000, true)}>提交表单 ) } 节流什么是节流

防抖动和节流本质是不一样的。防抖动是多次触发但只会执行一次,节流是多次触发但周期内只会执行一次

throttling,节流的策略是,每个时间周期内,不论触发多少次事件,也只执行一次动作。上一个时间周期结束后,又有事件触发,开始新的时间周期,同样新的时间周期也只会执行一次动作。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指周期结束后执行动作,前缘是指执行动作后再开始周期。

延迟throttling示意图:

image.png

前缘throttling 示意图:

image.png

实现节流函数

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:(前缘throttling)

import React from 'react' export default function ThrottleTest() { let throttle = (func: any, wait: any) => { // func为原本事件触发后所需要执行的函数、wait为节流的周期时长 let prev: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间 return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错 let context = this; // 谁调用函数(这里的函数是节流函数return出来的函数),this就指向谁。 // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数。 // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。 let args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是节流函数return出来的函数)。 // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了节流函数,事件真正执行的函数是节流函数return出来的函数,只有它才能拿到event参数。 // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把节流函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了 let now = Date.now(); // 获取当前时间 if (!prev || now - prev >= wait) { // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束 console.log('新周期开始'); func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func prev = Date.now(); // 更新新周期的起始事件 } } } // 要执行的事件函数 const handleClick = (e: any) => { console.log('提交表单', e); } return ( throttle(handleClick, 3000)}>提交表单 ) }

时间戳版本与定时器版本的可配置结合(配置前缘throttling以及延迟throttling):

import React from 'react' export default function ThrottleTest() { const throttle = (func: any, wait: any, options: any) => { // func为原本事件触发后所需要执行的函数; // wait为节流的周期时长; // options是一个对象,如果想周期内第一次触发事件就执行func(即前缘throttling),传入{leading: false},如果想周期内最后一次触发事件才执行func(即延迟throttling),传入{trailing: false},两者不能共存,否则函数不能执行 let timeout: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储定时器,以便判断当前是否处于定时状态(防抖状态),因此借助闭包缓存定时器实例 let context: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。 // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。因此借助闭包缓存调用者的this let args: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数。 // 事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。 // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了。因此借助闭包缓存事件函数参数(比如event参数) let result: any = null; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储func执行之后返回的结果,因此借助闭包缓存func执行之后返回的结果 let previous = 0; // 为了避免全局的命名污染,因此我们不考虑使用全局变量。但同时我们需要一个变量来存储一个新周期的起始时间,因此借助闭包缓存新周期的起始时间 // 如果 options 没传则设为空对象 if (!options) options = {}; let later = function() { previous = options.leading === false ? 0 : Date.now(); // 如果是设定周期内第一次触发事件就执行func,那么就重置previous为0,否则就将previous设为当前时间 timeout = null; // 清空context、args闭包变量,释放内存 result = func.apply(context, args); // 通过 apply 修改func的this指向,并让func获取真正的事件函数(即节流函数return出来的函数)的参数,之后执行func if (!timeout) context = args = null; // 清空context、args闭包变量,释放内存 }; return function(this: any) { // 备注:"this: any" 是ts的写法,如果不传this,下方语句“context = this;”会报错,js的写法不传this也不会报错 let now = Date.now(); if (!previous && options.leading === false) previous = now; // 如果当前周期起始时间不存在(那就是第一次触发事件),或者当前周期已经结束 let remaining = wait - (now - previous); context = this; // 谁调用函数(这里的函数是防抖函数return出来的函数),this就指向谁。 // 这步不能少,因为原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数。 // 此时func的this指向是window,如果func内部想修改this指向当前函数的调用者,就必须存储this,之后借助apply修改func的this指向。 args = arguments; // arguments 是一个对应于传递给函数的参数的类数组对象,可以获取函数的参数(这里的函数是防抖函数return出来的函数)。 // 这步不能少,因为某些事件函数是自带event参数的(比如click、input),而此时原本的事件函数func,我们是作为参数传递给了防抖函数,事件真正执行的函数是防抖函数return出来的函数,只有它才能拿到event参数。 // 而原本的事件函数func是拿不到event参数的,如果少了这步,不把防抖函数return出来的函数的arguments对象存起来的话 func就获取不到event参数了 if (remaining wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; // 要执行的事件函数 const handleClick = (e: any) => { console.log('提交表单', e); } return ( trailing: false})}>提交表单 ) } 防抖与节流注意事项

①使用闭包

为了避免全局的命名污染,因此我们不考虑使用全局变量。同时为了让所需变量能得到缓存,因此我们使用闭包存储部分需要用到的变量

②this指向的问题以及event参数:

因为事件触发调用的,是防抖/节流函数return出来的函数,而不是事件函数func,事件函数func是作为参数传入防抖/节流函数,其this指向是window,而不是触发事件调用函数的dom,同理,自然也拿不到事件自带的event参数,所以我们要通过闭包,存储this上下文以及argument这一传递给函数的参数的类数组对象

实际上,所有的高阶函数,内部都需要注意this的绑定

文章参考

https://blog.csdn.net/hupian1989/article/details/80920324

https://segmentfault.com/a/1190000018428170

https://www.jianshu.com/p/c8b86b09daf0

https://www.cnblogs.com/youma/p/10559331.html



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3