1.前言
宝藏秘籍书太火爆,被借的太多,所以一直被打扰,一直敲门,导致我们必须要限流。我们会在门口出示借书规则,不要一直敲门,工作日每两个小时会开放借一本书(节流);休息日,提示更是不要一直敲门,印刷员工需要休息,如果一直敲门,就会一直借不到书,敲门之后等待两个小时,印刷工在开门借书,如果在两个小时之内又敲门,则会重新计时(防抖)。这样既保证每天都能借到书,又要照顾我们印刷员工的情绪。
2.详解
- 防抖:事件触发 n 秒后,回调才会执行,如果在 n 秒内再次触发,则会重新计时
- 节流:事件触发 n 秒后,回调才会执行,如果在 n 秒内持续触发,在 n 秒间隔之内只执行一次
3.适用场景
-
防抖适用场景:关心的是最终状态结果
- 用户输入完成后搜索
- 调整窗口大小后重新布局
-
节流适用场景:关心的是定期反馈中间状态结果
- 鼠标滚动事件获取滚动位置
- 鼠标移动跟踪鼠标位置
- 频繁触发点击事件
4.优缺点
4.1 防抖的优缺点
- 防抖的优点
- 聚焦最终状态,减少中间无用调用次数
- 防抖的缺点
- 实时场景需求上,延迟执行可能造成卡顿
4.2 节流的优缺点
- 节流的优点
- 优化性能,减少函数无效调用次数,在固定时间间隔内又有反馈
- 节流的缺点
- 可能冗余,在事件间隔内停止触发,可能还会在执行最后一次
5.代码实现
5.1 防抖代码实现
- 使用闭包保存定时器的状态,使得状态持久化,设置参数回调函数和延迟时间
javascript
const debounce = (func,delay) => {
let timeoutId;
const debounced = function(...args) {
//每次执行前,需要清除定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
//调用函数如果重新绑定 this,支持 this 能够正确绑定
func.apply(this,args);
}, delay);
}
return debounced
}
- 支持立即执行配置
ini
const debounce = (func,delay,immediate) => {
let timeoutId;
const debounced = function(...args) {
//每次执行前,需要清除定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
if(immediate) {
func.apply(this,args);
immediate = false;
return;
}
timeoutId = setTimeout(() => {
//调用函数如果重新绑定 this,支持 this 能够正确绑定
func.apply(this,args);
}, delay);
}
return debounced
}
- 支持取消函数方法
scss
const debounce = (func,delay,immediate) => {
let timeoutId;
const debounced = function(...args) {
if (timeoutId) {
clearTimeout(timeoutId);
}
if(immediate) {
func.apply(this,args);
immediate = false; // 立即执行后,设置为false
return;
}
timeoutId = setTimeout(() => {
//调用函数如果重新绑定 this,支持 this 能够正确绑定
func.apply(this,args);
}, delay);
}
// 取消函数继续执行
debounced.cancel = () => {
if(timeoutId) {
clearTimeout(timeoutId);
}
}
return debounced
}
5.2 节流代码实现
- 使用时间戳控制执行频率,如果等待时间大于间隔时间,则执行回调事件。1 秒内多次执行只执行一次
ini
const throttle = (func, limit) => {
let timeoutId,lastExcuted = 0;
if(!limit || limit < 1000){ limit = 1000; } // 最小限制为1000毫秒
return function(...args) {
const wait = limit - (Date.now() - lastExcuted);
if (wait <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null
}
func.apply(this, args);
lastExcuted = Date.now();
}
}
}
- 支持第一次触发立即执行
ini
const throttle = (func, limit,options = {leading: true}) => {
let timeoutId,lastExcuted = 0;
if(!limit || limit < 1000){ limit = 1000; } // 最小限制为1000毫秒
return function(...args) {
const wait = limit - (Date.now() - lastExcuted);
//支持灵活配置是否首次执行
if(!lastExcuted && !leading) {
lastExcuted = Date.now()
}
if (wait <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null
}
func.apply(this, args);
lastExcuted = Date.now();
}
}
}
- 支持尾部执行,在间隔时间内的最后一次触发会执行
ini
const throttle = (func, limit,options = {leading: true,trailing: true}) => {
let timeoutId,lastExcuted = 0;
if(!limit || limit < 1000){ limit = 1000; } // 最小限制为1000毫秒
return function(...args) {
const wait = limit - (Date.now() - lastExcuted);
//支持灵活配置是否首次执行
if(!lastExcuted && !leading) {
lastExcuted = Date.now()
}
if (wait <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null
}
func.apply(this, args);
lastExcuted = Date.now();
} else if (trailing) {
// 如果设置了trailing,使用setTimeout来延迟执行
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
result = func.apply(this, args);
resolve(result);
lastExcuted = Date.now();
timeoutId = null
}, wait);
}
}
}
- 保持与回调函数的返回值一致,尾部执行属于异步调用执行,我们可以使用 Promise来接收函数返回值
ini
const throttle = (func, limit,options = { leading: true, trailing: true }) => {
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}
let lastExcuted = 0;
let timeoutId;
if(!limit || limit < 1000){ limit = 1000; } // 最小限制为1000毫秒
const { leading = true, trailing = true } = options
return function(...args) {
return new Promise((resolve, reject) => {
let result;
//首次不执行
if(!lastExcuted && !leading) {
lastExcuted = Date.now();;
}
const wait = limit - (Date.now() - lastExcuted);
if (wait <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null
}
result = func.apply(this, args);
resolve(result);
lastExcuted = Date.now();
} else if (trailing ) {
// 如果设置了trailing,使用setTimeout来延迟执行
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
result = func.apply(this, args);
resolve(result);
lastExcuted = Date.now();
timeoutId = null
}, wait);
}
})
}
}
6.测试验证
防抖与节流都进行了单元测试,使用了Jest
框架,下面是测试的几个测试用例,完全通过
6.1防抖函数单元测试
scss
// debounce.test.js
const debounce = require('../4.debounce');
// Jest内置的定时器模拟
jest.useFakeTimers();
describe('debounce 防抖函数', () => {
let mockFunc; // 用于测试的模拟函数
beforeEach(() => {
// 重置模拟函数
mockFunc = jest.fn();
});
afterEach(() => {
// 清除所有定时器
jest.clearAllTimers();
});
test('延迟时间内多次调用,只执行最后一次', () => {
const debouncedFunc = debounce(mockFunc, 100);
// 连续调用3次
debouncedFunc('第一次');
debouncedFunc('第二次');
debouncedFunc('第三次');
// 此时定时器未触发,函数不应执行
expect(mockFunc).not.toHaveBeenCalled();
// 快进时间到100ms(刚好触发定时器)
jest.advanceTimersByTime(100);
// 验证只执行了最后一次调用
expect(mockFunc).toHaveBeenCalledTimes(1);
expect(mockFunc).toHaveBeenCalledWith('第三次');
});
test('两次调用间隔超过延迟时间,都会执行', () => {
const debouncedFunc = debounce(mockFunc, 100);
// 第一次调用
debouncedFunc('第一次');
// 快进150ms(超过延迟时间,触发第一次执行)
jest.advanceTimersByTime(150);
expect(mockFunc).toHaveBeenCalledTimes(1);
expect(mockFunc).toHaveBeenCalledWith('第一次');
// 第二次调用(与第一次间隔超过100ms)
debouncedFunc('第二次');
// 再快进100ms
jest.advanceTimersByTime(100);
expect(mockFunc).toHaveBeenCalledTimes(2);
expect(mockFunc).toHaveBeenCalledWith('第二次');
});
test('延迟时间准确', () => {
const delay = 200;
const debouncedFunc = debounce(mockFunc, delay);
debouncedFunc();
// 快进199ms(未到延迟时间)
jest.advanceTimersByTime(199);
expect(mockFunc).not.toHaveBeenCalled();
// 再快进1ms(总时间200ms)
jest.advanceTimersByTime(1);
expect(mockFunc).toHaveBeenCalledTimes(1);
});
test('能正确传递参数和this指向', () => {
const context = { name: '测试对象' };
const debouncedFunc = debounce(function(arg1, arg2) {
expect(this).toBe(context); // 验证this指向
expect(arg1).toBe('参数1');
expect(arg2).toBe('参数2');
}, 100);
// 绑定上下文并传参
debouncedFunc.call(context, '参数1', '参数2');
jest.advanceTimersByTime(100);
});
test('立即执行函数,第一次调用立即执行', () => {
const debouncedFunc = debounce(mockFunc, 100, true);
// 第一次调用
debouncedFunc('第一次');
expect(mockFunc).toHaveBeenCalledTimes(1);
expect(mockFunc).toHaveBeenCalledWith('第一次');
// 第二次调用
debouncedFunc('第二次');
// 再快进100ms
jest.advanceTimersByTime(100);
expect(mockFunc).toHaveBeenCalledTimes(2);
expect(mockFunc).toHaveBeenCalledWith('第二次');
});
test('取消函数,函数不在执行', () => {
const debouncedFunc = debounce(mockFunc, 100);
// 第一次调用
debouncedFunc('第一次');
// 取消防抖函数
debouncedFunc.cancel();
// 再快进100ms
jest.advanceTimersByTime(100);
expect(mockFunc).not.toHaveBeenCalled();
});
});
6.2节流函数单元测试
scss
// throttle.test.js
const throttle = require('../5.throttle');
// 模拟定时器
jest.useFakeTimers();
describe('节流函数测试', () => {
let callback;
let throttledFunc;
let returnFunc;
beforeEach(() => {
// 重置模拟函数
callback = jest.fn();
// 创建带返回值的测试函数
returnFunc = jest.fn((num) => {
return num * 2; // 返回输入值的2倍
});
// 清除所有定时器
jest.clearAllTimers();
});
// 测试默认配置(leading=true, trailing=true)
test('默认配置下,函数应按指定间隔执行并在最后触发一次', () => {
throttledFunc = throttle(callback, 1000);
// 连续触发5次,每次间隔20ms
throttledFunc(1); // t=0
throttledFunc(2); // t=20
throttledFunc(3); // t=40
throttledFunc(4); // t=60
throttledFunc(5); // t=80
// 第一次调用应立即执行
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(1);
// 快进到99ms,仍在第一个间隔内
jest.advanceTimersByTime(990);
expect(callback).toHaveBeenCalledTimes(1); // 未到间隔,不执行
// 快进到100ms,第一个间隔结束
jest.advanceTimersByTime(10);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith(5); // 最后一次的参数
});
// 测试leading=false(不立即执行)
test('leading=false时,首次调用不应立即执行', () => {
throttledFunc = throttle(callback, 1000, { leading: false });
throttledFunc(1); // t=0
expect(callback).not.toHaveBeenCalled(); // 不应立即执行
// 快进50ms,触发第二次调用
jest.advanceTimersByTime(500);
throttledFunc(2);
// 快进50ms,达到间隔时间
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(2); // 应使用最后一次调用的参数
});
// 测试trailing=false(不执行最后一次)
test('trailing=false时,间隔结束后不应执行最后一次', () => {
throttledFunc = throttle(callback, 1000, { trailing: false });
throttledFunc(1); // t=0
expect(callback).toHaveBeenCalledTimes(1); // leading=true,立即执行
// 快进50ms,触发第二次调用
jest.advanceTimersByTime(500);
throttledFunc(2);
// 快进50ms,达到间隔时间
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1); // trailing=false,不执行
});
// 测试this上下文绑定
test('节流函数应正确绑定this上下文', () => {
const context = { value: 10 };
function testFunc() {
callback(this.value);
}
throttledFunc = throttle(testFunc, 1000);
// 使用call绑定上下文
throttledFunc.call(context);
expect(callback).toHaveBeenCalledWith(10);
});
// 测试间隔外的调用
test('超过间隔时间的调用应重新执行', () => {
throttledFunc = throttle(callback, 1000);
throttledFunc(1); // t=0
expect(callback).toHaveBeenCalledTimes(1);
// 快进150ms,超过间隔时间
jest.advanceTimersByTime(1500);
throttledFunc(2); // t=150
expect(callback).toHaveBeenCalledTimes(2); // 应再次执行
expect(callback).toHaveBeenCalledWith(2);
});
// 测试长时间连续调用
test('长时间连续调用应按间隔规律执行', () => {
throttledFunc = throttle(callback, 1000);
// 每20ms调用一次,持续500ms
for (let i = 0; i < 25; i++) {
throttledFunc(i);
jest.advanceTimersByTime(200);
}
//总时长5000ms,预期执行6次(0:0ms, 4:1000ms,:9:2000ms,:14:3000ms,:19:4000ms,:24:5000ms)
expect(callback).toHaveBeenCalledTimes(6);
});
// 测试默认模式下的返回值
test('默认模式下应正确返回函数执行结果', async () => {
throttledFunc = throttle(returnFunc, 1000);
// 第一次调用(立即执行)
const result1 = await throttledFunc(1);
expect(result1).toBe(2);
expect(returnFunc).toHaveBeenCalledWith(1);
// 快进到1000ms(节流间隔结束)
jest.advanceTimersByTime(1000);
const result2 = await throttledFunc(2); // 这个调用会触发尾部执行
expect(result2).toBe(4); // 应返回第二次调用的结果
expect(returnFunc).toHaveBeenCalledWith(2);
});
// 测试leading=false模式的返回值
test('leading=false时应返回延迟执行的结果', async () => {
throttledFunc = throttle(returnFunc, 1000, { leading: false });
// 第一次调用(不会立即执行)
const promise1 = throttledFunc(1);
expect(returnFunc).not.toHaveBeenCalled();
// 推进到100ms
jest.advanceTimersByTime(1000);
// 应返回第一次调用的结果
expect(await promise1).toBe(2);
expect(returnFunc).toHaveBeenCalledWith(1);
});
// 测试间隔外调用的返回值
test('超过间隔后调用应返回新的执行结果', async () => {
throttledFunc = throttle(returnFunc, 1000);
// 第一次调用
const result1 = await throttledFunc(1);
expect(result1).toBe(2);
// 推进150ms(超过节流间隔)
jest.advanceTimersByTime(1500);
// 新的调用应立即执行并返回新结果
const result2 = await throttledFunc(2);
expect(result2).toBe(4);
expect(returnFunc).toHaveBeenCalledTimes(2);
});
// 测试this上下文对返回值的影响
test('正确绑定this上下文时的返回值', async () => {
const context = {
multiplier: 3,
calculate: function(num) {
return num * this.multiplier;
}
};
throttledFunc = throttle(context.calculate, 1000);
// 绑定上下文调用
const result = await throttledFunc.call(context, 2);
expect(result).toBe(6); // 2 * 3 = 6
});
});
7.总结
防抖函数 是属于延迟执行,在防抖函数最后一次触发,等待间隔时间后执行,适用于比较关心最终结果状态的需求;节流函数属于定期执行,在固定时间频率上执行,适用于中间有实时反应状态的需求