在前端开发中,有一部分用户行为会频繁的触发事件,而对于DOM操做,资源加载等耗费性能的处理,极可能会致使卡顿,甚至浏览器的崩溃。防抖和节流就是为了解决这一类的问题。javascript
window.onscroll = function () { //滚动条位置 let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滚动条位置:' + scrollTop); }
从效果上,咱们能够看到,在页面滚动的时候,会在短期内触发屡次绑定事件。html
咱们知道DOM操做是很耗费性能的,若是在监听中,作了一些DOM操做,那无疑会给浏览器形成大量性能损失。前端
下面咱们进入主题,一块儿来探究,如何对此进行优化。java
理解:在车站上车,人员上满了车才发走重点是人员上满触发一次。浏览器
缘由缓存
定义:屡次触发事件后,事件处理函数只执行一次,而且是在触发操做结束时执行。闭包
原理:对处理函数进行延时操做,若设定的延时到来以前,再次触发事件,则清除上一次的延时操做定时器,从新定时。app
参考连接函数
// 计时器 var timer = false; // window.onscroll = function(){ clearTimeout(timer); timer = setTimeout(function(){ console.log("防抖"); console.log(new Date()); },300); };
为何要clearTimeoutoop
每次onscroll的时候,先清除掉计时器.若是不清楚,会致使屡次触发的时候,实际上是把好屡次的处理方法放在某个时间点后一块儿执行。
好比下面: for (var i = 0; i < 10; i++) { (function (i) { setTimeout(function () { console.log(i); }, 3000); })(i); }
上面代码在3秒后会一块儿输出 1,2,3,4,5,6,7,8,9
而下面的代码,只会输出9
var timer2 = false; for (var i = 0; i < 10; i++) { clearTimeout(timer2); (function (i) { timer2 = setTimeout(function () { console.log(i); }, 3000); })(i); }
这是由于,每次我将上次的timer给清除掉了,也就是我若是后面一样有处理函数的话,那我就用后面的定时器。
前面定时器没啥用了,因此直接clearTimeout保证了这种实现。
在解决onscroll问题的时候,若是本身观察console能够发现,防抖保证了滚动中止的时候,才会进行处理,由于滚动中止了,没有scroll事件了,最后一次timer会被保留,从而进行调用
//每一次都要清空定时器,从新设置上计时器值,使得计时器每一次都从新开始,直到最后知足条件而且等待delay时间后,才开始执行handler函数。 let timer ; window.onscroll = function(){ if(timer){ clearTimeout(timer); } timer = setTimeout(function(){ //滚动条位置 let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滚动条位置'+ scrollTop); timer = null; },2000) console.log(timer);//一滚动,timer就有对应的数值,即 //setTimeout会返回数值,clearTimeout(这个数值),就至关于清除这个定时器
效果以下:滚动结束触发事件
// func是用户传入须要防抖的函数 // wait是等待时间 const debounce = (func, wait = 50) => { // 缓存一个定时器id let timer = 0 // 这里返回的函数是每次用户实际调用的防抖函数 // 若是已经设定过定时器了就清空上一次的定时器 // 开始一个新的定时器,延迟执行用户传入的方法 return function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { func.apply(this, args) }, wait) } } window.onscroll = debounce(function(){ let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滚动条位置:' + scrollTop); } ,200) // 不难看出若是用户调用该函数的间隔小于wait的状况下,上一次的时间还未到就被清除了,并不会执行函数
这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:
例如在搜索引擎搜索问题的时候,咱们固然是但愿用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它老是在一连串(间隔小于wait的)函数触发以后调用。
例如用户给interviewMap点star的时候,咱们但愿用户点第一下的时候就去调用接口,而且成功以后改变star按钮的样子,用户就能够立马获得反馈是否star成功了,这个状况适用当即执行的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
// 这个是用来获取当前时间戳的 function now() { return +new Date() } /** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */ function debounce (func, wait = 50, immediate = true) { let timer, context, args // 延迟执行函数 const later = () => setTimeout(() => { // 延迟函数执行完毕,清空缓存的定时器序号 timer = null // 延迟执行的状况下,函数会在延迟函数中执行 // 使用到以前缓存的参数和上下文 if (!immediate) { func.apply(context, args) context = args = null } }, wait) // 这里返回的函数是每次实际调用的函数 return function(...params) { // 若是没有建立延迟执行函数(later),就建立一个 if (!timer) { timer = later() // 若是是当即执行,调用函数 // 不然缓存参数和调用上下文 if (immediate) { func.apply(this, params) } else { context = this args = params } // 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个 // 这样作延迟函数会从新计时 } else { clearTimeout(timer) timer = later() } } }
对于==按钮防点击来==说的实现:若是函数是当即执行的,就当即调用,若是函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都从新计时。一旦你点累了,定时器时间到,定时器重置为 null,就能够再次点击了。
对于延时执行函数来讲的实现:清除定时器ID,若是是延迟调用就调用函数
经过上面的例子,咱们知道咱们能够经过函数防抖,解决了屡次触发事件时的性能问题。
好比,咱们在监听滚动条位置,控制是否显示返回顶部按钮时,就能够将防抖函数应用其中。
当咱们作图片懒加载(lazyload)时,须要经过滚动位置,实时显示图片时,若是使用防抖函数,懒加载(lazyload)函数将会不断被延时,
只有停下来的时候才会被执行,对于这种须要实时触发事件的状况,就显得不是很友好了。
下面开始介绍函数节流,经过设定时间片,控制事件函数间断性的触发。
理解:大于等于10分钟发一次车,重点是必定间隔时间就会触发一次。
定义:触发函数事件后,短期间隔内没法连续调用,只有上一次函数执行后,过了规定的时间间隔,才能进行下一次的函数调用。
原理:对处理函数进行延时操做,若设定的延时到来以前,再次触发事件,则清除上一次的延时操做定时器,从新定时。
简单来讲,函数的节流就是经过闭包保存一个标记(canRun = true),在函数的开头判断这个标记是否为true,若是为true的话就继续执行函数,不然则 return 掉,判断完标记后当即把这个标记设为false,而后把外部传入的函数的执行包在一个setTimeout中,最后在setTimeout执行完毕后再把标记设置为true(这里很关键),表示能够执行下一次的循环了。
当setTimeout还未执行的时候,canRun这个标记始终为false,在开头的判断中被 return 掉。
function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); }; }
scroll 的一个简单例子
let startTime = Date.now(); //开始时间 let time = 500; //间隔时间 let timer; window.onscroll = function throttle(){ let currentTime = Date.now(); if(currentTime - startTime >= time){ let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滚动条位置:' + scrollTop); startTime = currentTime; }else{ clearTimeout(timer); timer = setTimeout(function () { throttle() }, 50); } }
- 效果以下:每隔500毫秒触发一次事件
/** * 节流函数 * @param method 事件触发的操做 * @param mustRunDelay 间隔多少毫秒须要触发一次事件 */ function throttle(method, mustRunDelay) { let timer, args = arguments, start; return function loop() { let self = this; let now = Date.now(); if(!start){ start = now; } if(timer){ clearTimeout(timer); } if(now - start >= mustRunDelay){ method.apply(self, args); start = now; }else { timer = setTimeout(function () { loop.apply(self, args); }, 50); } } } window.onscroll = throttle(function () { let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滚动条位置:' + scrollTop); },800)
上面的代码一刷新就触发事件,即打印出'滚动条位置"
** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 若是想忽略开始函数的的调用,传入{leading: false}。 * 若是想忽略结尾函数的调用,传入{trailing: false} * 二者不能共存,不然函数不能执行 * @return {function} 返回客户调用函数 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 以前的时间戳 var previous = 0; // 若是 options 没传则设为空对象 if (!options) options = {}; // 定时器回调函数 var later = function() { // 若是设置了 leading,就将 previous 设为 0 // 用于下面函数的第一个 if 判断 previous = options.leading === false ? 0 : _.now(); // 置空一是为了防止内存泄漏,二是为了下面的定时器判断 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 得到当前时间戳 var now = _.now(); // 首次进入前者确定为 true // 若是须要第一次不执行函数 // 就将上次时间戳设为当前的 // 这样在接下来计算 remaining 的值时会大于0 if (!previous && options.leading === false) previous = now; // 计算剩余时间 var remaining = wait - (now - previous); context = this; args = arguments; // 若是当前调用已经大于上次调用时间 + wait // 或者用户手动调了时间 // 若是设置了 trailing,只会进入这个条件 // 若是没有设置 leading,那么第一次会进入这个条件 // 还有一点,你可能会以为开启了定时器那么应该不会进入这个 if 条件了 // 其实仍是会进入的,由于定时器的延时 // 并非准确的时间,极可能你设置了2秒 // 可是他须要2.2秒才触发,这时候就会进入这个条件 if (remaining <= 0 || remaining > wait) { // 若是存在定时器就清理掉不然会调用二次回调 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判断是否设置了定时器和 trailing // 没有的话就开启一个定时器 // 而且不能不能同时设置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };