前端中的防抖和节流具体实现
哈喽 大家好!最近把面试总经常问到的防抖和节流知识整体总结一下,下面就分享给大家:
先介绍一下什么是防抖和节流,有两个问题可以有助于我们理解抽象的概念:为什么需要防抖和节流?哪些场景有都应用?
我们可以从这两个问题来介绍防抖和节流,从生活中举例子来理解这两个抽象概念
防抖执行的函数不会立即执行,例如:用户在输入框中频繁输入内容,搜索或者提交信息;频繁的点击某一个按钮,触发某个事件;监听浏览器的滚动事件,完成某些特定操作等等。
节流执行的函数会按照一定频率或者周期去执行,例如:用户频繁点击按钮操作、监听页面的滚动事件、鼠标移动事件等等。
防抖的原理是什么?
当某一个事件触发时,相应的函数不会立即触发,而是会等待一定时间,再去触发,例如:当用户在搜索框输入时需要请求请求服务器,如果每输入一个文字去请求服务器一定会降低服务器性能、增加服务器处理的压力,因为请求请求需要发送发给服务器很多次,只有等待了一段时间后用户没有再去触发事件,才会真正的执行响应函数;基于这个原理,我可以分步骤简单来实现一下防抖函数。(当然这里使用第三方库很多,例如:loadsh、underscore)
节流的原理又是什么?
当事件触发时,会执行这个事件的响应函数,如果这个事件会被频繁的触发,那么节流函数会按照一定的频率来执行,不管在这个中间有多少次触发事件,执行函数的频率总是固定不变的。(在某一段时间触发的事件是非常多的,但是我们只在某段时间只触发一次)
防抖函数的简单实现:
HTML代码:
<input type="text" class="text" /> <button class="btn">取消</button>
CSS代码:
input { width: 300px; height: 40px; line-height: 40px; font-size: 20px; } button { width: 100px; height: 45px; font-size: 20px; }
JS代码:
/** * 没有参数的节流函数 * @param {*} fn * @param {*} delay * @returns */ function debounce_A(fn, delay) { let timer = null; // 真正执行的函数 const _debounce = function () { if (timer) { // 取消上一次的定时器 clearTimeout(timer); } timer = setTimeout(function () { fn(); }, delay); }; return _debounce; } /** * 带有参数的节流函数 * @param {*} fn * @param {*} delay * @returns */ function debounce_B(fn, delay) { let timer = null; // 真正执行的函数 const _debounce = function (...args) { if (timer) { // 取消上一次的定时器 clearTimeout(timer); } timer = setTimeout(() => { // 外部传入的真正要执行函数 fn.apply(this, args); }, delay); }; return _debounce; } /** * 第一次是否立即会执行节流函数 和 取消执行 * @param {*} fn * @param {*} delay * @param {*} immediate */ function debounce_C(fn, delay, immediate = false) { let timer = null; let isInvoke = false; // 真正执行的函数 const _debounce = function (...args) { if (timer) { // 取消上一次的定时器 clearTimeout(timer); } if (immediate && !isInvoke) { // 第一次要执行的函数 fn.apply(this, args); isInvoke = true; } else { timer = setTimeout(() => { // 外部传入的真正要执行函数 fn.apply(this, args); // 间隔了一会儿,又想起来要输入,第一次需要被执行 isInvoke = false; }, delay); } }; // 取消功能 _debounce.cancel = function () { if (timer) { clearTimeout(timer) } timer = null isInvoke = false } return _debounce; }
分别对三个功能简单测试一下:
var btn = document.querySelector(".btn"); var text = document.querySelector(".text"); var count = 0; const inputChange = function (event) { console.log(`第${++count}次网络请求`, this, event); }; // text.oninput = debounce_A(inputChange, 2000); // text.oninput = debounce_B(inputChange, 2000); const debounceChange = debounce_C(inputChange, 2000, true); text.oninput = debounceChange; btn.addEventListener("click", function () { console.log(`已经取消第${++count}次网络请求`, this, event); debounceChange.cancel(); });
节流函数的简单实现:
/** * 基本实现 * @param {*} fn * @param {*} interval * @returns */ function throttle_A(fn, interval) { // 记录上一次开始的时间 let lastTime = 0; const _throttle = function () { // 2.获取当前函数触发的时间 let nowTime = new Date().getTime(); // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间 let remainTime = interval - (nowTime - lastTime); if (remainTime <= 0) { fn(); // 4.保留上次触发的时间 lastTime = nowTime; } }; return _throttle; }
/** * 解决第一次执行函数后,如果再次触发就会立即执行,应该是第一次执行函数后,再次触发时,需要判断是否超过延时时间 * leading :控制第一次要不要去执行的功能 * @param {*} fn * @param {*} interval * @param {*} options * @returns */ function throttle_B( fn, interval, options = { leading: true, trailing: false } ) { // 记录上一次开始的时间 let lastTime = 0; let timer = null; let { leading, trailing } = options; const _throttle = function () { // 2.获取当前函数触发的时间 let nowTime = new Date().getTime(); // 2.1 如果是第一次执行才会去触发, 把第一次/上一次执行的执行时间记录 if (!lastTime && !leading) { lastTime = nowTime; } // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间 let remainTime = interval - (nowTime - lastTime); if (remainTime <= 0) { fn(); // 4.保留上次触发的时间 lastTime = nowTime; } }; return _throttle; }
/** * 是否延迟执行 * @param {*} fn * @param {*} interval 是否需要立即执行 * @param {*} options * @returns */ function throttle_C( fn, interval, options = { leading: true, trailing: false } ) { // 记录上一次开始的时间 let lastTime = 0; let timer = null; // leading:开始是否触发 , trailing:最后一次是否会执行 let { leading, trailing } = options; const _throttle = function (...args) { // 2.获取当前函数触发的时间 let nowTime = new Date().getTime(); // 2.1 如果是第一次执行才会去触发, 把第一次/上一次执行的执行时间记录 if (!lastTime && !leading) { lastTime = nowTime; } // 3.真正需要执行的时间,当前时间减去上一次执行过的时间,再用延迟时间减去这个时间,就是这一次需要真正执行的时间 let remainTime = interval - (nowTime - lastTime); if (remainTime <= 0) { if (timer) { clearTimeout(timer); timer = null; } // 3.1 真正执行触发的函数 fn.apply(this, args); // 4.保留上次触发的时间 lastTime = nowTime; return; } // 5. 判断最后一次是否会执行 if (trailing && !timer) { timer = setTimeout(() => { timer = null; // 控制最后一次执行的时间 lastTime = leading ? new Date().getTime() : 0; fn.apply(this, args); }, remainTime); } }; return _throttle; }