未加星标

【从0到0.1系列】节流与防抖

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2019 | 作者 红领巾 ] 0人收藏点击收藏

本是机械开关的“去弹跳”概念,弹簧开关按下后,由于簧片的作用,接触点会连续接触断开好多次,如果每次接触都通电对用电器不好,所以就要控制按下到稳定的这段时间不通电

前端开发中则是一些频繁的事件触发

鼠标( mousemove ...)键盘( keydown ...)事件等 表单的实时校验(频繁发送验证请求)

在 debounce 函数没有再被调用的情况下经过 delay 毫秒后才执行回调函数,例如

在 mousemove 事件中,确保多次触发只调用一次监听函数 在表单校验的时候,不加防抖,依次输入 user ,就会分成 u , us , use , user 四次发出请求;而添加防抖,设置好时间,可以实现完整输入 user 才发出校验请求 2. 思路

由 debounce 的功能可知防抖函数至少接收两个参数(流行类库中都是 2 个参数)

fn delay

debounce 函数 返回一个闭包 ,闭包被频繁的调用

fn

使用闭包是为了使指向定时器的变量不被 gc 回收

实现在延时时间 delay 内的连续触发都不执行回调函数 fn ,使用的是在闭包内设置定时器 setTimeOut 频繁调用这个闭包,在每次调用时都要将上次调用的定时器清除 被闭包保存的变量就是指向上一次设置的定时器 3. 实现

符合原理的简单实现

function debounce(fn, delay) { var timer; return function() { // 清除上一次调用时设置的定时器 // 计时器清零 clearTimeout(timer); // 重新设置计时器 timer = setTimeout(fn, delay); }; } 复制代码

简单实现的代码,可能会造成两个问题

this 指向问题。debounce 函数在定时器中调用回调函数 fn ,所以 fn 执行的时候 this 指向全局对象(浏览器中 window ),需要在外层用变量将 this 保存下来,使用 apply 进行显式绑定

function debounce(fn, delay) { var timer; return function() { // 保存调用时的this var context = this; clearTimeout(timer); timer = setTimeout(function() { // 修正 this 的指向 fn.apply(this); }, delay); }; } 复制代码

event 对象。javascript 的事件处理函数中会提供事件对象 event ,在闭包中调用时需要将这个事件对象传入

function debounce(fn, delay) { var timer; return function() { // 保存调用时的this var context = this; // 保存参数 var args = arguments; clearTimeout(timer); timer = setTimeout(function() { console.log(context); // 修正this,并传入参数 fn.apply(context, args); }, delay); }; } 复制代码 4. 完善( underscore 的实现)

立刻执行。增加第三个参数,两种情况

先执行回调函数 fn ,等到停止触发后的 delay 毫秒,才可以再次触发( 先执行 ) 连续的调用 debounce 函数不触发回调函数,停止调用经过 delay 毫秒后才执行回调函数( 后执行 ) clearTimeout(timer) 后, timer 并不会变成 null ,而是依然指向定时器对象 function debounce(fn, delay, immediate) { var timer; return function() { var context = this; var args = arguments; // 停止定时器 if (timer) clearTimeout(timer); // 回调函数执行的时机 if (immediate) { // 是否已经执行过 // 执行过,则timer指向定时器对象,callNow 为 false // 未执行,则timer 为 null,callNow 为 true var callNow = !timer; // 设置延时 timer = setTimeout(function() { timer = null; }, delay); if (callNow) fn.apply(context, args); } else { // 停止调用后delay时间才执行回调函数 timer = setTimeout(function() { fn.apply(context, args); }, delay); } }; } 复制代码

返回值与取消 debounce 函数

回调函数可能有返回值。 undefined 能取消 debounce 函数。一般当 immediate 为 true 的时候,触发一次后要等待 delay 时间后才能再次触发,但是想要在这个时间段内想要再次触发,可以先取消掉之前的 debounce 函数 function debounce(fn, delay, immediate) { var timer, result; var debounced = function() { var context = this; var args = arguments; // 停止定时器 if (timer) clearTimeout(timer); // 回调函数执行的时机 if (immediate) { // 是否已经执行过 // 执行过,则timer指向定时器对象,callNow 为 false // 未执行,则timer 为 null,callNow 为 true var callNow = !timer; // 设置延时 timer = setTimeout(function() { timer = null; }, delay); if (callNow) result = fn.apply(context, args); } else { // 停止调用后delay时间才执行回调函数 timer = setTimeout(function() { fn.apply(context, args); }, delay); } // 返回回调函数的返回值 return result; }; // 取消操作 debounced.cancel = function() { clearTimeout(timer); timer = null; }; return debounced; } 复制代码

ES6 写法

function debounce(fn, delay, immediate) { let timer, result; // 这里不能使用箭头函数,不然 this 依然会指向 windows对象 // 使用rest参数,获取函数的多余参数 const debounced = function(...args) { if (timer) clearTimeout(timer); if (immediate) { const callNow = !timer; timer = setTimeout(() => { timer = null; }, delay); if (callNow) result = fn.apply(this, args); } else { timer = setTimeout(() => { fn.apply(this, args); }, delay); } return result; }; debounced.cancel = () => { clearTimeout(timer); timer = null; }; return debounced; } 复制代码 :books: throttle 1. 概念

固定函数执行的速率

如果持续触发事件,每隔一段时间,执行一次事件

例如监听 mousemove 事件时,不管鼠标移动的速度,【节流】后的监听函数会在 wait 秒内最多执行一次,并以此【匀速】触发执行

window 的 resize 、 scroll 事件的优化等

2. 思路

有两种主流实现方式

使用时间戳 设置定时器

节流函数 throttle 调用后 返回一个闭包

闭包用来保存之前的时间戳或者定时器变量(因为 变量被返回的函数引用,所以无法被垃圾回收机制回收 )

时间戳方式

当触发事件的时候,取出当前的时间戳,然后减去之前的时间戳(初始设置为 0) 结果大于设置的时间周期,则执行函数,然后更新时间戳为当前时间戳 结果小于设置的时间周期,则不执行函数

定时器方式

当触发事件的时候,设置一个定时器 再次触发事件的时候,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器 设置下个定时器

将两种方式结合,可以实现兼并立刻执行和停止触发后依然执行一次的效果

3. 实现

时间戳实现

function throttle(fn, wait) { var args; // 前一次执行的时间戳 var previous = 0; return function() { // 将时间转为时间戳 var now = +new Date(); args = arguments; // 时间间隔大于延迟时间才执行 if (now - previous > wait) { fn.apply(this, args); previous = now; } }; } 复制代码 previous

定时器实现

function throttle(fn, wait) { var timer, context, args; return function() { context = this; args = arguments; // 如果定时器存在,则不执行 if (!timer) { timer = setTimeout(function() { // 执行后释放定时器变量 timer = null; fn.apply(context, args); }, wait); } }; } 复制代码 回调函数不会立刻执行,要在 wait 秒后第一次执行,停止触发闭包后,如果停止时间在两次执行之间,则还会执行一次

结合时间戳和定时器实现

function throttle(fn, wait) { var timer, context, args; var previous = 0; // 延时执行函数 var later = function() { previous = +new Date(); // 执行后释放定时器变量 timer = null; fn.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = +new Date(); // 距离下次执行 fn 的时间 // 如果人为修改系统时间,可能出现 now 小于 previous 情况 // 则剩余时间可能超过时间周期 wait var remaining = wait - (now - previous); context = this; args = arguments; // 没有剩余时间 || 修改系统时间导致时间异常,则会立即执行回调函数fn // 初次调用时,previous为0,除非wait大于当前时间的时间戳,否则剩余时间一定小于0 if (remaining <= 0 || remaining > wait) { // 如果存在延时执行定时器,将其取消掉 if (timer) { clearTimeout(timer); timer = null; } previous = now; fn.apply(context, args); if (!timeout) context = args = null; } else if (!timer) { // 设置延时执行 timer = setTimeout(later, remaining); } }; return throttled; } 复制代码 过程中的节流功能是由时间戳的原理实现,同时实现了立刻执行 定时器只是用来设置在最后退出时增加一个延时执行 定时器在每次触发时都会重新计时,但是只要不停止触发,就不会去执行回调函数 fn 4. 优化完善

增加第三个参数,让用户可以自己选择模式

{ leading: false } { trailing: false }

增加返回值功能

增加取消功能

function throttle(func, wait, options) { var context, args, result; var timeout = null; // 上次执行时间点 var previous = 0; if (!options) options = {}; // 延迟执行函数 var later = function() { // 若设定了开始边界不执行选项,上次执行时间始终为0 previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; // func 可能会修改 timeout 变量 result = func.apply(context, args); // 定时器变量引用为空,表示最后一次执行,则要清除闭包引用的变量 if (!timeout) context = args = null; }; var throttled = function() { var now = new Date().getTime(); // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。 if (!previous && options.leading === false) previous = now; // 延迟执行时间间隔 var remaining = wait - (now - previous); context = this; args = arguments; // 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口 // remaining 大于时间窗口 wait,表示客户端系统时间被调整过 if (remaining <= 0 || 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; }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; } 复制代码 有个问题, leading: false 和 trailing: false 不能同时设置 第一次开始边界不执行,但是,第一次触发时, previous 为 0,则 remaining 值和 wait 相等。所以, if (!previous && options.leading === false) 为真,改变了 previous 的值,而 if (remaining <= 0 || remaining > wait) 为假 以后再触发就会导致 if (!previous && options.leading === false) 为假,而 if (remaining <= 0 || remaining > wait) 为真。就变成了开始边界执行。这样就和 leading: false 冲突了 :books: 总结 至此,完整实现了一个 underscore 中的 debounce 函数和 throttle 函数 而 lodash 中 debounce 函数和 throttle 函数的实现更加复杂,封装更加彻底 推荐两个可视化执行过程的工具 demo.nimius.net/debounce_th… caiogondim.github.io/js-debounce… 自己实现是为了学习其中的思想,实际开发中尽量使用 lodash 或 underscore 这样的类库。 对比

throttle 和 debounce 是解决请求和响应速度不匹配问题的两个方案。二者的差异在于选择不同的策略

电梯超时现象解释两者区别。假设电梯设定为 15 秒,不考虑容量限制

throttle debounce

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:【从0到0.1系列】节流与防抖
本站链接:https://www.codesec.net/view/628431.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(157)