1.引言
1.1 何为抖动:
- 首先我想说一下什么叫做抖动,抖动现象在日常的开发是很常见的一个现象。抖动是由于高频事件触发而产生的性能问题。
- 这种问题通常发生在用户交互密集的场景下,比如窗口的
size
变化,频繁滚动网页或在输入框快速打字等操作。 - 如果不加以限制的话,会导致系统资源的过度消耗,从而严重影响应用的响应速度和用户的使用体验。
2.频繁事件处理的问题
2.1 性能瓶颈:
- 当高频的事件频发触发时,它们会要求浏览器进行大量的计算和重绘,这将显著增加
DOM
的操作次数,最终导致页面卡顿,甚至崩溃。 - 例如,当用户在窗口中调整大小或滚动页面时,浏览器需要重新计算布局和渲染页面,这会消耗大量的
CPU
资源。 - 如果这些事件处理函数非常复杂,或者处理的数据量非常大,那么性能问题会更加严重。
2.2 用户体验影响:
- 频繁的
DOM
操作和网络请求不仅消耗了系统的资源,还会导致用户界面的卡顿和反应迟钝。 - 例如,用户在滚动长页面的时候,如果浏览器忙于处理每一次滚动事件导致的
DOM
更新的话,就会给用户一种“卡住”的感觉。 - 同样地,如果一个输入框在修改时频繁发送网络请求,那么用户的输入会显得非常迟钝,这也会影响用户的体验,降低用户的满意度。
3.防抖(Debounce)技术
3.1 概念阐释:
- 所以为了解决抖动这个性能问题,我们就需要去做一些优化策略。那么防抖就出现了。
- 防抖其实本质上就是一种函数调用优化策略,就是当一个事件被频繁触发的时候,只有在最后一次事件发生后的指定时间内
- 没有新的事件触发时,才会执行相应的函数。
- 这就表示即使用户频繁触发同一事件,该函数也只会执行一次,从而避免了函数的频繁调用,导致的抖动现象。
3.2 应用场景:
防抖技术适用于那些短时间内连续触发相同事件的场景
,常见的应用场景包括:
-
搜索框输入实时搜索:当用户每一次更新搜索框内容时,每敲击一个字符都会触发输入事件,如果立刻发送网络请求获取搜索结果,这样会导致大量的无谓的网络请求。
所以可以使用防抖技术当用户停止输入指定时间后,再发送网络请求,这样可以大大减少网络请求的次数,提高性能。
-
窗口大小调整:当用户调整浏览器窗口大小时,浏览器会频繁触发resize事件,如果每次resize事件都执行一些复杂的计算,那么就会导致浏览器卡顿。
所以可以使用防抖技术,当用户停止调整窗口大小时,再执行
resize
事件的处理函数,这样可以避免频繁的DOM
操作,提高性能。
3.3 实现方法:
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
上面是一个简易版的防抖函数
- 这个防抖函数接受两个参数,第一个参数是要执行的函数
func
,第二个参数是等待时间wait
。 - 在函数内部,我们定义了一个变量
timeout
,用来存储定时器的ID
。 - 当防抖函数被调用时,我们首先清除之前的定时器(如果存在的话),然后设置一个新的定时器,在等待时间wait后执行函数
func
。 - 这样,无论防抖函数被调用多少次,只有在最后一次调用后的等待时间
wait
内没有新的调用时,函数func
才会被执行。
3.4 进阶技巧:
-
防抖函数还可以接受一个可选的第三个参数
immediate
,用来指定是否在事件触发时立即执行函数func
。 -
如果
immediate
为true
,那么函数func
会在事件触发时立即执行,然后等待wait
时间后,如果不再有新的事件触发,那么函数func
会再次执行。 -
如果
immediate
为false
,那么函数func
会在事件触发后的wait
时间后执行,如果在这段时间内有新的事件触发,那么函数func会重新计时。 -
下面是一个带有
immediate
参数的防抖函数的实现:
function debounce(func, wait, immediate) {
let timeout;
return function(...args) {
clearTimeout(timeout);
if (immediate && !timeout) {
func.apply(this, args);
}
timeout = setTimeout(() => {
if (!immediate) {
func.apply(this, args);
}
timeout = null;
}, wait);
};
}
- 在实际使用中,我们还可以根据需求对防抖函数进行一些定制,比如可以根据不同的场景动态调整等待时间, 例如基于事件类型或用户行为的复杂性,或者添加一些额外的参数等。
- 总之,防抖技术是一种非常实用的函数调用优化策略,可以帮助我们解决抖动问题,提高应用的性能和用户体验。
4.节流(Throttle)技术
4.1 概念对比:
- 节流和防抖都是用来优化函数调用的技术,但是它们的工作原理和用途是不同的。
- 节流技术是一种限制函数执行频率的策略,它会在指定的时间间隔内只执行一次函数,无论在这段时间内函数被调用了多少次。
- 这就表示即使用户频繁触发同一事件,该函数也只会以固定的频率执行,从而避免了函数的频繁调用,导致的抖动现象。
4.2 应用场景:
节流技术适用于那些需要限制函数执行频率的场景
,常见的应用场景包括:
- 滚动加载更多:当用户滚动页面时,浏览器会频繁触发
scroll
事件,如果每次scroll
事件都执行一些复杂的计算,那么就会导致浏览器卡顿。所以可以使用节流技术,可以确保处理函数在一定时间内只执行一次,保持页面滚动的流畅性。 - 游戏帧率控制:在游戏开发中,需要控制游戏帧率,以保证游戏的流畅性。可以使用节流技术,当游戏帧率超过指定值时,只执行一次游戏逻辑,这样可以避免游戏帧率过高,导致游戏卡顿。
- 数据上传/下载:在网络请求中,频繁的数据上传或下载可能会导致网络拥塞。通过节流限制上传或下载的频率,可以优化网络带宽的使用。
4.3 实现代码:
- 实现节流的方法有两种,一种是使用时间戳,另一种是使用定时器。
1. 时间戳实现:
const lastTime = 0;
const throttle = function(func, wait){
const now = new Date();
if(now - lastTime >= wait){
func();
lastTime = now;
}
}
- 上面时间戳的实现方式是记录上一次执行时的时间,
- 然后每次调用函数时,获取当前时间,如果当前时间与上一次执行时间的差值大于等于等待时间,就执行函数,并更新上一次执行时间。
2. 定时器实现:
let timeoutId;
const throttle = function(func, wait){
if(!timeoutId){
timeoutId = setTimeout(() => {
func();
timeoutId = null;
}, wait);
}
}
-
上面定时器的实现方式是使用一个定时器,当第一次调用函数时,设置一个定时器,在等待时间后执行函数,并将定时器
ID
保存下来。 -
如果在这段时间内再次调用函数,那么会清除之前的定时器,并重新设置一个新的定时器。
-
这样,无论函数被调用了多少次,只有在等待时间后才会执行一次函数。
-
但是使用
setTimeout
函数我们之前也说过会造成调用的延迟。 -
在这里你会发现为啥感觉和防抖有点像,但是仔细分析来看两者是有区别的。
- 防抖是每一次进入的时候直接清除
setTimeout
的Id
,接着执行setTimeout
函数并且重新设置setTimeout
的Id
。这里相当于重新计时。 - 节流是每一次进入的时候都去判断
setTimeout
的Id
是否存在,如果存在则不执行,如果不存在则执行并且设置setTimeout
的Id
。这里相当于隔开一段时间。
- 防抖是每一次进入的时候直接清除
4.4 高级用法:
在节流技术中,领先触发(leading
)和尾随触发(trailing
)是两个重要的概念:
领先触发:函数在时间间隔的开始立即执行,即在事件首次触发时就执行函数,随后在设定的时间间隔内不再执行,直到下一个周期开始。
尾随触发:函数在时间间隔的结束时执行,即在事件连续触发时,函数不会立即执行,而是等待时间间隔结束后才执行一次。
5.防抖与节流的选择
5.1 决策指南:
选择防抖还是节流,主要取决于你的具体需求和应用场景。以下是几个决策准则:
防抖:
当你需要在用户完成某个动作(如停止输入、停止滚动
)后的一段时间再执行操作时,应使用防抖。
特别适用于需要减少网络请求或计算开销的场景,比如实时搜索、窗口尺寸调整、或在用户输入完成后执行异步验证
。
节流:
当你需要控制函数的执行频率,确保在一定时间间隔内函数只执行一次时,应使用节流。
特别适用于处理频繁触发的事件,如滚动事件、鼠标移动、或需要平滑动画效果的场合。
5.2 综合运用:
在某些复杂场景下,你可能需要同时使用防抖和节流。 例如,在实现一个复杂的输入控件时,你可能希望在用户输入时实时显示搜索建议(使用节流来控制频率), 并在用户停止输入后自动提交搜索(使用防抖来等待用户完全停止输入)。
6.实战案例
6.1 实战需求:
下面我们来玩一个具体的需求:
-
实现无限滚动功能,并且在用户接近页面底部时自动加载更多内容
-
实现输入框中实时显示搜索建议
6.2 实战分析:
-
首先我们要确定这两个具体的需求要根据上面的决策准测,来确定两个需求来选择防抖还是节流还是两者组合
-
在用户接近页面底部时自动加载更多内容,那么就表示我们只需要在一定时间间隔内去获取内容就可以,并不需要它疯狂滚动时一直频繁的调用接口导致卡顿问题,所以这里我们用节流的方法即可。
-
输入框中输入内容接着调用接口,这个动作后的一段时间再去调用接口即可,并不需要用户在修改的时候就频繁的调用接口,所以这里我们只需要使用防抖即可
6.3 实战代码:
-
HTML代码:
<body> <div id="search-container"> <input type="text" id="search-input" placeholder="Search..."> <ul id="suggestions"></ul> </div> <div id="content"> <div>初始内容</div> </div> </body>
-
JAVASCRIPT代码:
<!--引入lodash库--> <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script> <script> // 1. 实现无限滚动功能,并且在用户接近页面底部时自动加载更多内容 /** * 使用isNearBottom函数检测是否接近页面底部。 * 使用_.throttle来限制loadMore函数的调用频率,避免在快速滚动时发送过多网络请求。 * 每次加载更多内容时,向页面底部追加10个条目。 */ /** * 判断的方法:窗口的高度的高度加上滚动位置和页面总高度进行比较。 * 如果滚动位置加上视口高度接近或等于页面总高度,则说明用户靠近了底部。 */ // 检查是否接近页面底部 function isNearBottom() { const triggerHeight = 100; // 触发加载的距离阈值 // 窗口高度 const windowHeight = window.innerHeight; // 滚动位置 const scrollPosition = document.documentElement.scrollTop || document.body.scrollTop; // 页面总高度 const pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight; console.log('是否靠近底部?', pageHeight - windowHeight - scrollPosition <= triggerHeight); return pageHeight - windowHeight - scrollPosition <= triggerHeight; } /** * 我们可以利用lodash库, 这个库提供了_.debounce和 _.throttle两个函数即防抖和节流 * _.debounce可以接受一个wait参数来设置延迟时间,以及一个immediate参数来控制是否在第一次调用时立即执行。 * _.throttle同样接受wait参数,但确保函数在设定的时间间隔内最多执行一次。 */ // 加载更多内容-使用节流 const loadMore = _.throttle(() => { console.log('加载更多内容'); fetch('https://jsonplaceholder.typicode.com/photos') // 使用一个公开的API作为示例 .then(response => response.json()) .then(data => { data.slice(0, 10).forEach(item => { const newItem = document.createElement('div'); newItem.textContent = item.title; document.getElementById('content').appendChild(newItem); }); }) .catch(error => console.error('错误:', error)); }, 500); // 2. 实现输入框中实时显示搜索建议 /** * 使用_.debounce来限制searchSuggestions函数的调用,避免在用户快速输入时发送过多网络请求。 * 每次用户输入时,清空并填充搜索建议列表。 */ const searchSuggestions = _.debounce(query => { fetch(`https://jsonplaceholder.typicode.com/users?username_like=${query}`) // 假设API支持这样的查询 .then(response => response.json()) .then(suggestions => { const suggestionsList = document.getElementById('suggestions'); suggestionsList.innerHTML = ''; suggestions.forEach(user => { const option = document.createElement('li'); option.textContent = user.username; suggestionsList.appendChild(option); }); }) .catch(error => console.error('错误:', error)); }, 300); // 页面加载完毕后注册事件监听器 window.addEventListener('load', () => { window.addEventListener('scroll', () => { if (isNearBottom()) { loadMore(); } }); const inputElement = document.getElementById('search-input'); inputElement.addEventListener('input', event => { searchSuggestions(event.target.value); }); }); </script>
-
CSS代码:
<style> body { height: 2000px; } #content { margin-top: 2500px; } #search-container { position: fixed; top: 10px; left: 10px; z-index: 1000; } </style>
8.总结与展望
8.1 总结:
-
防抖和节流出现的背景就是抖动,抖动就是高频事件触发而产生的性能问题。
-
防抖和节流都是防止抖动的一种优化策略,但是它们两者又是从不同维度来解决这个抖动问题的。
-
防抖是从调用的时间来限制的,每一次有新的调用时候都会清空之前的计时,重新开始计时,直到满足最后一次的调用之后的一段时间之后才会去调用。这样就算用户频繁去做一个
action
的时候,函数只会调用一次。 -
节流是从调用频率的维度来解决抖动这个问题的。它的简易实现方法有时间戳法和
setTimeout
法,这两个方法也有所不同。- 时间戳法就是设置一个常用变量去标记上一次调用的时间,然后根据现在调用的时间进行相减,再和设置的
wait
时间比较,超过wait
的时间即可调用,最后灵魂一步将常用变量上一次调用的时间设置当前的时间即可。 setTimeout
法,最灵魂的东西就是setTimeout
的Id
,当这个id
为null
的时候我们就去调用方法,接着设置id
,这样id
就不会为null
了,就能控制它的调用了。
- 时间戳法就是设置一个常用变量去标记上一次调用的时间,然后根据现在调用的时间进行相减,再和设置的
-
防抖和节流开发者还可以使用
lodash
库,这个库里面有已经封装好的_.debounce
(防抖)和_.throttle
(节流)方法,这里面可以传入wait参数去控制调用的时间间隔。 当然,开发者也可以自己开发定制的函数去实现,作者也在高级方法下面提到过。 -
防抖和节流本身这两者并没有冲突,虽然它们的应用环境有所不同,只需要根据判断准则即可满足开发者的需要,并且这两者也可以组合使用。 根据不同的场景,开发者权衡去使用。
8.2 主流框架防抖和节流的实现:
- React:有专门的库如
react-throttle
和react-debounce
,以及高阶组件(HOCs
)和自定义hooks
如useDebounce
和useThrottle
,开发者可以在服务或组件中使用这些关键字或者库来防止抖动。 - Vue.js:
Vue
自身的v-model
指令允许使用修饰符来实现节流效果。同时,有像vue-debounce
和vue-throttle-debounce
这样的插件,以及自定义的Vue
指令和mixins
可以直接在模板和组件中使用。 - Angular:
RxJS
提供了debounceTime
和throttleTime
等操作符,可以很容易地在Angular
应用中集成防抖和节流。开发者可以在服务或组件中使用这些操作符来处理异步数据流。
这些主流框架防抖和节流的玩法,少侠们自己去玩玩吧!后面玩到具体的框架的时候,我也会出相关的博客。