前言
日前我参加了字节跳动的面试。面试官看着我的简历,沉思了一下,说:"手写个节流函数给我瞅瞅,写得好的话,直接给你offer!"
我心里"咯噔"一声,原来一个小小的节流函数,就能直接决定我能不能拿到offer!看来不能掉以轻心,一定要写出一个最优雅和专业的节流函数给面试官瞅瞅!
什么是节流
在面试官提出的问题中,"手写节流函数" 早在2017年就在很多前端博客里出现,节流函数的应用在前端开发中是至关重要的,它可以确保我们的代码在处理频繁的事件时不会导致性能问题:JS执行和网页的重新渲染是互斥的。
节流函数的核心思想是:控制函数的执行频率,确保函数在一定时间间隔内只被执行一次。这意味着无论用户多频繁地触发某个事件,函数都只会在规定的时间内执行一次。这对于处理滚动事件、输入事件等非常有用。网上的答案也是层出不穷,有ES5版本的,有ES6版的,有用定时器做的,也有用时间戳做的,面试前复习过一遍,脑海里依稀记得大概长下面这个样子:
js
// 时间戳版本
function throttle(fun, interval) {
let lastTimestap = 0; //初始时间
return function (...args) {
let currentTimestap = new Date(); //当前时间
if (currentTimestap - lastTimestap > interval) {
fun.apply(this, args);
lastTimestap = currentTimestap;
}
};
}
// 定时器版本
function throttle(fn, interval) {
let lock = false;
return function (...args) {
if (!lock) {
lock = true;
setTimeout(() => {
lock = false;
fn.apply(this, args);
}, interval);
}
};
}
然而,就在我充满自信地准备默写时间戳版本的时候,面试官放了一张图并加了三个节流函数的额外限制条件:
- 可以控制首次的回调函数是否立即调用
- 距离本周期结束最近的一个回调函数,会被延迟到本周期结束时执行
- 可以控制最后的回调是否延迟执行
尊嘟假嘟?第一次还能配置不立即执行?那我是不是得写定时器版本的节流?最后一次还能配置是否延迟执行?我措手不及,被颠覆了认知,陷入了窘境。我能感受到面试官的一丝幽默,他拿着答案看着我做不出来,笑了起来。这一刻,我明白了前端开发的真正精髓,不仅在于掌握基本概念,还在于能否现场解决难题。
实现节流
实现后续回调可延迟执行
既然是首次回调立即执行,那么肯定不能换定时器版本的节流,必须得在时间戳版本的节流上加额外的分支,来处理在interval内触发多次回调的情况(在interval节点上触发的情况已经处理了,也就是立即执行),其实延迟执行的本质就是使用定时器,延迟到下一个interval时间点上执行就实现了,延迟多久呢?看图可得 remaining = interval - (currentTimestap - lastTimestap)
,多次触发的时候记得清空上一轮的定时器,因为定时器只能存在一个
js
function throttle(fn, interval) {
let lastTimestap = 0,
timer = null;
return function (...args) {
let currentTimestap = new Date();
// 清空上一轮的计时器
clearTimeout(timer);
if (currentTimestap - lastTimestap >= interval) {
fn.apply(this, args);
lastTimestap = currentTimestap;
} else {
// 使用计时器把最近的回调延时触发
const remaining = interval - (currentTimestap - lastTimestap);
timer = setTimeout(() => {
fn.apply(this, args);
lastTimestap = new Date();
}, remaining);
}
};
}
配置后续回调可延迟执行
添加一个options参数{ trailing: true }
,用来判断处不处理在interval内触发多次回调的情况就行了
js
function throttle(fn, interval, options = { trailing: true }) {
let lastTimestap = 0,
timer = null;
return function (...args) {
let currentTimestap = new Date();
clearTimeout(timer);
if (currentTimestap - lastTimestap >= interval) {
fn.apply(this, args);
lastTimestap = currentTimestap;
} else if (options.trailing) { // 根据options.trailing来判断走不走这个分支逻辑就行了
const remaining = interval - (currentTimestap - lastTimestap);
timer = setTimeout(() => {
fn.apply(this, args);
lastTimestap = new Date();
}, remaining);
}
};
}
配置首次回调可立即执行
为了做到在任何周期内,次次都进入到延迟执行的逻辑中,那就得让currentTimestap - lastTimestap >= interval
不成立,比如在初始化的时候让lastTimestap = currentTimestap
,直接进入定时器的逻辑(lastTimestap初始值为0)
js
function throttle(fn, interval, options = { leading: true, trailing: true }) {
let lastTimestap = 0,
timer = null;
return function (...args) {
let currentTimestap = new Date();
clearTimeout(timer);
// 让lastTimestap初始值变为currentTimestap进入定时器逻辑
if (!options.leading && !lastTimestap) lastTimestap = currentTimestap;
// options.leading处理leading和trailing同时为false的情况
if (options.leading && currentTimestap - lastTimestap >= interval) {
fn.apply(this, args);
lastTimestap = currentTimestap;
} else if (options.trailing) {
const remaining = interval - (currentTimestap - lastTimestap);
timer = setTimeout(() => {
fn.apply(this, args);
// 定时器结束后重置lastTimestap为0
lastTimestap = options.leading ? new Date() : 0;
}, remaining);
}
};
}
测试
js
const sleep = (time) =>
new Promise((resolve) => {
setTimeout(() => {
console.log(`${time}ms 后`);
resolve();
}, time);
});
const throttleLog1 = throttle((val) => console.log(val), 1000, {
leading: true,
trailing: true,
});
const throttleLog2 = throttle((val) => console.log(val), 1000, {
leading: false,
trailing: true,
});
const throttleLog3 = throttle((val) => console.log(val), 1000, {
leading: true,
trailing: false,
});
const throttleLog4 = throttle((val) => console.log(val), 1000, {
leading: false,
trailing: false,
});
// 周期: 0 1 2 3
// 回调: 1 34 2 5
throttleLog1(1);
await sleep(900);
throttleLog1(3);
throttleLog1(4);
await sleep(900);
throttleLog1(2);
await sleep(500);
throttleLog1(5);
// 1
// 900ms 后
// 4
// 900ms 后
// 2
// 500ms 后
// 5
throttleLog2(1);
await sleep(900);
throttleLog2(3);
throttleLog2(4);
await sleep(900);
throttleLog2(2);
await sleep(500);
throttleLog2(5);
// 900ms 后
// 4
// 900ms 后
// 500ms 后
// 5
throttleLog3(1);
await sleep(900);
throttleLog3(3);
throttleLog3(4);
await sleep(900);
throttleLog3(2);
await sleep(500);
throttleLog3(5);
// 1
// 900ms 后
// 900ms 后
// 2
// 500ms 后
throttleLog4(1);
await sleep(900);
throttleLog4(3);
throttleLog4(4);
await sleep(900);
throttleLog4(2);
await sleep(500);
throttleLog4(5);
// 900ms 后
// 900ms 后
// 500ms 后
最后
面试官看到我能如此流畅地实现节流函数,眼前一亮,大声说道:"你这代码写得真是太牛了,快把offer拿走吧!"我终于长出了一口气,靠着扎实的编程技巧,一举拿下了心仪的offer!我开心地站起来准备接过offer,却听到一阵刺耳的闹钟声......原来刚才都是在做梦!不过梦里的奇思妙想还是让我收获颇丰呢!