关键词:定时器 性能优化 防抖节流 倒计时
防抖节流解决什么问题?
本质其实就是控制函数在一段时间内被执行的次数。
| 防抖 | 节流 | |
|---|---|---|
| 区别 | 最后只会调用最后一次 | 每隔一定时间调用一次函数 |
| 相同 | 防止函数多次调用 | 防止函数多次调用 |
| 应用场景 | input/click | resize/touchmove/mousemove/scroll |
防抖是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。
比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。
开发中最常用的就是防抖,表单输入、提交按钮等。
节流的概念是指一定时间内函数只运行一次。 throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。
比如我们排队做核酸,一个窗口一分钟只能做一个核酸。
开发中最常见的例子就是用户向下滚动无限加载的页面/表格,需要定时检查用户离底部的距离,然后判断是否需要请求数据append 进列表。这种情况防抖就不适合了,防抖是在用户停止滚动时触发,而我们需要在用户到达底部之前酒开始获取数据。
函数防抖
- 函数在一段时间内多次触发只会执行第一次,在这段时间结束前,不管触发多少次也不会执行函数。
- 在等待时间内触发此函数,则重新计算等待时间。
函数节流
- 在操作结束后才执行
- 如果超过了设定的时间,就执行一次处理函数。
react hooks 如何实现?
Vue 实现防抖
不要直接在组件的 method 选项中创建防抖函数,然后在 template 中调用这些方法作为事件处理器。
原因是组件使用 export default { ... } 导出的 options 对象,包括方法,会被组件实例重用。如果网页中有 2 个以上的组件实例,那么所有的组件都会应用相同的防抖函数 methods.debouncedHandler 会导致防抖出现故障。
倒计时
setInterval:如果 代码 执行时间比设定的时间间隔还要常,建议递归调用 setTimeout。因为 JS 是单线程语言,如果前面有阻塞线程的任务,例如:网络延迟或者服务器无响应等问题。会导致 setInterval 延迟,这样倒计时就肯定会不准确。
setTimeout:有很多因素会导致 setTimeout 的回调函数执行比设定的预期值更久,比如嵌套超时、非活动标签超时、追踪型脚本的节流、超时延迟。
// setInterval
function countdown(endTime, cb) {
let t = endTime;
const timer = setInterval(() => {
t = t - 1000;
if(t <= 0 ) {
clearInterval(timer)
return
}
cb(t)
}, 1000);
}
countdown(10000, (t) => console.log(t))
// setTimeout
function countdown(endTime, cb) {
let t = endTime;
const timer = setTimeout(() => {
t -= 1000;
if (t > 0) {
cb(t)
countdown(t, cb);
return false
}
clearTimeout(timer);
}, 1000)
}
countdown(10000, (t) => console.log(t))
利用 requestAnimationFrame 结合 setTimeout 来优化。
requestAnimationFrame,浏览器API,允许以 60 帧/秒 (FPS) 的速率请求回调,而不会阻塞主线程。
performance.now():返回值表示为从time origin之后到当前调用时经过的时间。
function countdown(endTime) {
const now = performance.now();
function start() {
const timer = setTimeout(() => {
const diff = endTime - (performance.now() - now);
console.log(diff)
if (diff <= 0) {
clearTimeout(timer);
} else {
requestAnimationFrame(start);
}
}, 1000)
}
start()
}
countdown(10000)
最后综合起来写一个通用的倒计时方法。
class CountDown {
constructor({ leftTime, ms = 1000, onEnd }) {
this.leftTime = leftTime;
this.ms = ms;
this.onEnd = onEnd;
this.countdownTimer = null;
this.startTime = performance.now();
this.nextTime = leftTime % ms;
this.totalTime = 0;
this.count = leftTime;
this.startCountDown();
}
clearTimer() {
if (this.countdownTimer) {
clearTimeout(this.countdownTimer);
this.countdownTimer = null;
}
}
startCountDown(nt = 0) {
this.clearTimer();
const executionTime = performance.now() - this.startTime;
this.totalTime += executionTime;
this.updateCount(executionTime, nt);
this.nextTime = this.ms - (this.totalTime % this.ms);
this.startTime = performance.now();
if (this.count <= 0) {
return false;
}
this.countdownTimer = setTimeout(() => {
requestAnimationFrame(() => this.startCountDown(0));
}, this.nextTime);
}
updateCount(executionTime, nt) {
this.count -= (Math.floor(executionTime / this.ms) || 1) * this.ms + nt;
if (this.count <= 0) {
this.count = 0;
this.clearTimer();
if (this.onEnd) {
this.onEnd();
}
}
}
}
// 使用示例
const countdown = new CountDown({
leftTime: 20000,
ms: 1000,
onEnd: () => console.log("倒计时结束"),
});
参考资料:
Debouncing and Throttling Explained Through Examples 这篇文章写得非常好,例子举得也好。