Vue3+Vite+VueRouter+Pinia+Axios+ElementPlus
最近项目有一个数据导出功能,之前后端都是同步处理,前端做防抖处理,数据量特别大的时候,造成响应时间特别长。这次后端做了优化处理,将后端做了异步处理,响应特别快,但是需要再额外的下载页面去处理导出,这样造成用户下载后直接进入下载页面需要等待一段时间才能看到下载文件,而且对于后端来讲,防抖虽然可以控制频率(默认1s),但是他想要把时间控制在 10s 左右,才能实现二次下载。如果继续使用防抖,如果直接定 10s ,就会造成第一次点击无效的点击,当然这个可以通过一个变量动态控制这个时间,但是稍显麻烦。所以我采用了节流,去实现他的需求,相对简单了很多。
函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。具体使用哪一个需要大家根据实际情况决定。
函数节流与函数防抖是解决频繁触发DOM事件的两种常用解决方案:
- 1.**基本思想:**某些代码不可以在没有间断的情况连续重复执行。
- 2. 使用原因:DOM 操作比起非 DOM 交互,需要更多的内存和 CPU 时间,连续尝试过多的 DOM 操作可能会导致浏览器挂起,甚至崩溃。
- 3. **应用场景:**只要代码是周期执行的,都应该节流。鼠标移动 mousemove 事件、滚动条滚动 scroll 事件,浏览器窗口改变 resize 事件等。
- 4. 防抖节流逻辑图
5.1 节流器(Throttle)
节流:在规定时间内,只触发一次。比如我们设定500ms间隔,在这个时间段内,无论点击按钮多少次,它都只会触发一次。
生活举例:一个水龙头在滴水,可能一次性会滴很多滴,但是我们只希望它每隔 500ms 滴一滴水,保持这个频率。即我们希望函数在以一个可以接受的频率重复调用。
具体场景:如 1.商品抢购环节,由于有无数人快速点击按钮,如果每次点击都发送请求,就会给服务器造成巨大的压力。2. 数据导出功能,当数据量特别大,一次点击后端需要耗费大量时间的处理才能响应数据,如果频繁点击导出按钮,严重可能会造成服务器崩溃。如果对以上场景进行节流处理,就会大大减少请求的次数,从而减轻服务器压力。
核心:在于让一个函数不要执行的太频繁,减少一些过快的操作,类似于王者荣耀中技能冷却时间。
5.1.1 使用时间戳封装节流器
/** * @param {*} fn 需要包装的事件回调 * @param {*} delay 每次推迟执行的等待时间 */ export function throttle(fn, delay = 1000) { // 距离上一次的执行时间 let latestTime = 0 return function () { const _this = this const _arguments = arguments const nowTime = new Date().getTime() // 如果距离上次执行超过了,delay才能再次执行 if (nowTime - latestTime > delay) { fn.apply(_this, _arguments) latestTime = nowTime } else { this.$message.warning(`下载频率过高,请${Math.ceil((delay - (nowTime - latestTime)) / 1000)}秒后再次下载!`) } } }
时间戳版本会优先执行,点击立即执行一次。也是这次项目导出功能优化采用的方式。
5.1.2 使用定时器封装节流器
export function throttle(fn, delay = 1000) { let timer = null return function () { const _this = this const _arguments = arguments if (!timer) { timer = setTimeout(function () { timer = null; fn.apply(_this, _arguments) }, delay) } } }
定时器版本会后置执行,点击需等待 delay 时间之后执行。之前做的防抖就是这个样子,后置执行的情况下,设置过长等待时间体验不好。
5.2 防抖(Debounce)
防抖:在连续的操作中,无论进行了多长时间,只有某一次的操作后在指定的时间内没有再操作,这一次才被判定有效。
生活举例:将一个弹簧按下,继续加压,继续按下,只会在最后放手的一瞬反弹。即我们希望函数只会调用一次,即使在这之前反复调用它,最终也只会调用一次而已。
具体场景:搜索框输入关键字过程中实时请求服务器匹配搜索结果,如果不进行处理,那么就是输入框内容一直变化,导致一直发送请求。如果进行防抖处理,结果就是当我们输入内容完成后,一定时间(比如500ms)没有再输入内容,这时再触发请求。
核心:在短时间内大量触发同一事件时,只会执行一次回调函数。避免把一次事件误认为多次。
封装防抖函数
/** * @param {*} fn 需要包装的事件回调 * @param {*} delay 每次推迟执行的等待时间 */ export function debounce(fn, delay = 1000){ // 定时器 let timer = null // 将debounce处理结果当作函数返回 return function() { // 保留调用时的 this 上下文 const _this = this // 保留调用时传入的参数 const _arguments = arguments // 每次事件被触发时,都去清除之前的旧定时器 if (timer) { clearTimeout(timer) } // 设立新定时器 timer = setTimeout(function() { fn.apply(_this, _arguments) }, delay) } }
5.3 常见使用场景
监听 scroll、mousemove 等事件 - 节流(每隔一秒计算一次位置)
监听浏览器窗口 resize 操作 - 防抖(只需计算一次)
键盘文本输入的验证 - 防抖(连续输入文字后发送请求进行验证,验证一次就好)
提交表单 - 防抖(多次点击变为一次)
search 搜索联想,用户在不断输入值时,用防抖来节约请求资源。
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
这里,以 resize 窗口改变事件 为例,以原生代码演示节流和防抖效果:
未使用防抖和节流的代码
window.onresize = function() { var div = document.getElementById('mydiv') div.style.height = div.offsetWidth + 'px' console.log('resize') }
不停的改变窗口大小,观察打印结果。。。。
很明显,用户如果不断放大缩小浏览器窗口,那我们监听函数将会不停的被调用,倘若函数过“重”,即假设如上文描述的一般,那么对浏览器的压力将会非常之大,其高频率的更改可能会让浏览器崩溃。
防抖处理
function debounce(fn, delay = 1000) { var timer = null; return function() { if(timer) { clearTimeout(timer) } var _arguments = arguments var _this = this timer = setTimeout(function() { fn.call(_this, _arguments) }, delay) } } function resizeDiv() { var div = document.getElementById('mydiv') div.style.height = div.offsetWidth + 'px' console.log('resize'); } window.onresize = debounce(resizeDiv)
触发事件,查看和之间的比较,确实解决了那个问题,但是,
放手之后这里才变化,显得有点突兀,那么能不能在我拖动的时候不能也让它变化同步呢???
节流处理
function resizeDiv(){ var div = document.getElementById('myDiv') div.style.height = div.offsetWidth + "px"; } function throttle(fn, delay = 1000){ clearTimeout(fn.timer); fn.timer = setTimeout(x=>{ fn.call(); }, delay) } window.onresize = function(){ throttle(resizeDiv) }
控制好处理频率,可以确保浏览器不会在极短的时间内进行过多次的计算。显然函数节流比防抖更适合窗口的高频变化。