看了《JavaScript高级程序设计》和网上的一些博客,感受对函数节流和函数防抖的概念是反的,如下我写的关于防抖和节流的概念取决于多数人的概念吧,而且基于伦敦前端工程师David Corbacho的客座文章。文章写的很好,而且有对应的代码能够操做,更容易理解。其实我以为叫什么不重要,这个方法叫节流仍是这个方法叫防抖,只要你能说明白,而且在生产中能用上就能够,一个名字,不用太去纠结。javascript
《复仇者联盟4:终局之战》表明着一个时代的结束,从2008年高二看300多MB的《钢铁侠》开始,漫威电影宇宙也像哈利波特的魔法世界同样一路伴我前行。一个时代的落幕,必将开始一个新的时代。End Game??No!css
I LOVE YOU THREE THOUSANDS TIMEShtml
I AM IRON MAN前端
防抖和节流是两个类似的技术,都是为了减小一个函数无用的触发次数,以便提升性能或者说避免资源浪费。咱们都知道js在操做DOM的时候,代价很是昂贵,相对于非DOM操做须要更多的内存和和CPU时间,假如咱们一个函数是在滚动滚动条或者更改更改窗口大小的时候频繁触发,仍是会出现页面卡顿,若是是一套复杂的操做DOM逻辑,可能还会引发浏览器崩溃。因此咱们须要控制一下触发的次数,来优化一下代码执行状况。java
口说无凭,你们可能也不了解究竟是怎样操做,那就来个例子:⬇️ node
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>我要节流</title>
<style> body{ height: 3000px; } #centerNum { width: 100px; height: 100px; line-height: 100px; text-align: center; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); } </style>
</head>
<body>
<h1 id="centerNum">0</h1>
<script> var num = 0; window.onscroll = function () { var root = document.getElementsByTagName('body'), h = document.getElementById('centerNum'); h.innerHTML = num; num ++; } </script>
</body>
</html>
复制代码
咱们来一个window.onscroll
的函数,只要滚动,就改变一次<h1>
标签中的数,在上面的图中,咱们能看到这个触发是很是频繁的,若是咱们不加以干涉的话,让这个函数肆意触发,岂不是要上天了😡api
啥是防抖呢?我本身的理解就是,当连续触发一个方法的时候,方法并不执行,而是在连续触发结束的时候再执行这个方法。浏览器
举个例子:一部直梯,陆续往上上人(连续触发),当再也不上人的时候(中止连续触发),电梯才会关门并动起来(执行方法)。前端工程师
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>电梯上人</title>
<style> </style>
</head>
<body>
<button id="addBtn">电梯上人,人数+1</button><button id="resetBtn">重置</button>
<p id="personNum">电梯人数:0(假设电梯能够无限装人)</p>
<p id="elevatorStatus">电梯停靠</p>
<script> var personNum = 0; // 电梯人数 var closeDoor = null; // 电梯启动延时程序 var addBtn = document.getElementById('addBtn'); // 获取添加人数按钮 var personNumP = document.getElementById('personNum'); // 获取显示人数的标签 var resetBtn = document.getElementById('resetBtn'); // 获取重置按钮 var elevatorStatus = document.getElementById('elevatorStatus'); // 获取电梯状态标签 /** * @method 电梯内添加人数 * @description 点击一次电梯内增长一人,增长完人数电梯启动初始化 */ function addPerson() { personNum ++; personNumP.innerHTML = `电梯人数:${personNum}(假设电梯能够无限装人)` initElevatorStart(); } /** * @method 电梯启动 * @description 电梯启动,置灰添加人数按钮,禁止上人 */ function elevatorStart() { elevatorStatus.innerHTML = '电梯启动'; addBtn.disabled = true; } /** * @method 电梯启动初始化 * @description 清除以前的关门延时,并从新计算关门延时500ms,意思是当不在触发电梯启动初始化函数时,500ms后启动电梯 */ function initElevatorStart() { clearTimeout(closeDoor); closeDoor = setTimeout(function () { elevatorStart(); }, 500); } /** * @method 重置电梯 */ function reset() { personNum = 0; personNumP.innerHTML = `电梯人数:${personNum}(假设电梯能够无限装人)` elevatorStatus.innerHTML = '电梯停靠'; addBtn.disabled = false; } addBtn.addEventListener('click', addPerson); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
复制代码
上面的代码意思就是我电梯上一我的,就须要关闭电梯门(触发initElevatorStart()
方法),而后电梯启动。可是我一直在点击上人的按钮,电梯是不会触发关门启动电梯的elevatorStart()
方法。闭包
代码的核心是initElevatorStart()
方法,这个方法在实际须要执行的关门启动电梯方法elevatorStart()
外面添加了一层setTimeout
方法,也就是为了在调用这个方法的时候咱们过500毫秒再去执行真正须要执行的方法。若是这500毫秒以内,又从新触发了initElevatorStart()
方法,就须要从新计时,要不不就夹到人了嘛,要赔钱的。。。。
这是防抖最粗糙的实现了😳😳😳
下面是这个防抖实现的最基本的形式,也是咱们在《JavaScript高级程序设计》中看到的样子⬇️
var processor = {
timeoutId: null, // 至关于延时setTimeout的一个标记,方便清除的时候使用
// 实际进行处理的方法
// 连续触发中止之后须要触发的代码
performProcessiong: function () {
// 实际执行的代码
// 这里实际就是须要在中止触发的时候执行的代码
},
// 初始处理调用的方法
// 在实际须要触发的代码外面包一层延时clearTimeout方法,以便控制连续触发带来的无用调用
process: function () {
clearTimeout(this.timeoutId); // 先清除以前的延时,并在下面从新开始计算时间
var that = this; // 咱们须要保存做用域,由于下面的setTimeout的做用域是在window,调用不要咱们须要执行的this.performProcessiong方法
this.timeoutId = setTimeout(function () { // 100毫秒之后执行performProcessiong方法
that.performProcessiong();
}, 100) // 若是尚未执行就又被触发,会根据上面的clearTimeout来清除并从新开始计算
}
};
// 尝试开始执行
processor.process(); // 须要从新绑定在一个触发条件里
复制代码
上面这段代码就是最基本的实现方式,包在一个对象中,而后在对象中互相调用,里面的注释应该能够很清楚的说明每一步是干什么呢,最下面的processor.process()
咱们在实际使用的时候确定是须要绑定在一个触发条件上的,好比以前的上电梯问题上,咱们就须要把processor.process()
方法绑定在增长人数的里面,这样才会有屡次调用的状况发生
上面再怎么说都是很简单的实现,在实际生产环境中,逻辑会相对复杂不少,可是万变不离其宗,参透了最基础的,再触类旁通就不是什么问题了
具体我也不知道应该叫啥,英文叫“Leading edge”,甭管中文叫啥了,知道是什么意思就好了。以前咱们写的代码很明显能够看出来,在咱们连续触发一个方法的时候,是在setTimeout
结束后才去真正执行,可是还有一种状况,那就是咱们在连续触发一个方法的时候,第一次触发就执行了,而后后面的连续触发再也不执行,等连续触发中止,通过延时之后,再次触发才会真正执行。
我仍是盗图吧。。。广泛的形式是下面这种
下面是我本身写的,大概意思是这样,代码实现也贴出来
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>电梯上人</title>
<style> </style>
</head>
<body>
<button id="addBtn">电梯上人,人数+1</button><button id="resetBtn">重置</button>
<p id="personNum">电梯人数:0(假设电梯能够无限装人)</p>
<script> var personNum = 0; // 电梯人数 var okNext = true; // 是否可进行下次执行 var timeoutFn = null; var addBtn = document.getElementById('addBtn'); // 获取添加人数按钮 var personNumP = document.getElementById('personNum'); // 获取显示人数的标签 var resetBtn = document.getElementById('resetBtn'); // 获取重置按钮 /** * @method 电梯添加人数 * @description 电梯能够上人,可是上人之后就不能再上了,无论怎么触发都不行,除非中止触发500毫秒之后,再触发的时候才能够继续执行 */ function addPerson() { if (okNext) { okNext = false; personNum ++ personNumP.innerHTML = `电梯人数:${personNum}(假设电梯能够无限装人)` } clearTimeout(timeoutFn); timeoutFn = setTimeout(function () { okNext = true; }, 500) } /** * @method 重置 */ function reset() { personNum = 0; personNumP.innerHTML = '电梯人数:0(假设电梯能够无限装人)'; } addBtn.addEventListener('click', addPerson); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
复制代码
上面代码要是看不太明白,能够直接粘下去本身执行如下看看是什么感受,就知道是什么意思了。
代码纯我本身写的,要是有不对的地方,请大佬指正啊
节流呢,也是我本身的理解,在连续触发一个方法的某一时间段中,控制方法的执行次数。
一样举个例子吧,一个地铁进站闸口,10秒进一我的(10秒内执行一个方法),管这10秒中来了是5我的、10我的仍是20我的,都只是进一我的(从第一次触发后10秒无论被触发多少次都不会执行,直到下一个10秒才会再执行)。
咱们首先用时间戳来判断先后的时间间隔,而后就能够知道我从上次执行完这个方法过了多久,过了这么长时间,是否是已经超过了本身规定的时长,若是时长超过了,我就能够再次执行了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>地铁进站</title>
</head>
<body>
<button id="addBtn">进站人数+1</button><button id="resetBtn">重置</button>
<p id="personTotal">旅客总人数:0</p>
<p id="personNum">进站人数:0</p>
<script> var personNum = 0; // 进站人数 var personTotal = 0; // 一共来了多少人 var addBtn = document.getElementById('addBtn'); // 获取添加人数按钮 var personNumP = document.getElementById('personNum'); // 获取显示人数的标签 var personTotalP = document.getElementById('personTotal'); // 获取显示总人数的标签 var resetBtn = document.getElementById('resetBtn'); // 获取重置按钮 /** * @method 增长进站人数 * @description 每一个时间间隔执行的方法 */ function addPerson() { personNum ++; personNumP.innerHTML = `进站人数:${personNum}`; } /** * @method 节流方法(时间戳) * @param {Function} fn 须要节流的实际方法 * @param {Number} wait 须要控制的时间长度 * @description 根据上一次执行的时间,和这一次执行的时间作比较,若是大于控制的时间,就能够执行 */ function throttle(fn, wait) { var prev = 0; // 第一次执行的时候是0,因此第一次点击的时候确定大于这个数,因此会立马执行 return function () { var context = this; var args = arguments; var now = Date.now(); // 实际执行的时间 personTotal ++; personTotalP.innerHTML = `旅客总人数:${personTotal}`; if (now - prev >= wait) { // 执行的时间是否是比上次执行的时间大于须要延迟的时间,大于,咱们就执行 fn.apply(context, args); prev = now; // 执行了之后,重置上一次执行的时间为刚刚执行此次函数的时间,下次执行就用这个时间为基准 } } } /** * @method 重置 */ function reset() { personNum = 0; personTotal = 0; personNumP.innerHTML = '进站人数:0'; personTotalP.innerHTML = `旅客总人数:0`; } addBtn.addEventListener('click', throttle(addPerson, 1000)); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
复制代码
节流函数throttle
用到了做用域,call、apply和闭包等相关的知识,看不懂的能够看我以前的文章
上面的代码中我感受能够很直观的看出来是根据判断先后两次的时间,来得知可不能够进行下一次函数的执行。参考着代码中的注释我以为应该能够看明白吧😳😳😳
若是咱们用setTimeout
的话,咱们只须要更改一下throttle
方法
/** * @method 节流方法(setTimeout) * @param {Function} fn 须要节流的实际方法 * @param {Number} wait 须要控制的时间长度 * @description 这个方法就很相似防抖了,就是判断当前函数有没有延迟setTimeout函数,有的话就不执行了 */
function throttle(fn, wait) {
var timeout = null;
return function () {
var context = this;
var args = arguments;
personTotal ++;
personTotalP.innerHTML = `旅客总人数:${personTotal}`;
if (!timeout) {
var that = this;
timeout = setTimeout(() => {
timeout = null;
fn.apply(context, args)
}, wait)
}
}
}
复制代码
虽然咱们只须要更改几行代码就实现了用setTimeout
实现节流的这个方法,可是咱们仔细看上面的图,咱们能够发现,当我点击第一次的时候,进站旅客是没有增长的,这跟咱们实际状况不同,咱们先来的,我不用等啊,我直接就能进站,对不对。还有当我结束增长人数的时候,进站旅客过去等待时间之后还会加一我的,这固然也不是咱们想看到的。
使用时间戳仍是setTimeout,取决于业务场景了
诶??rAF是什么?什么是requestAnimationFrame?这在我没有写这篇博客的时候,我根本不知道window下还有个这个方法,神奇吧,那这个方法是干什么的呢??
告诉浏览器——你但愿执行一个动画,而且要求浏览器在下次重绘以前调用指定的回调函数更新动画。该方法须要传入一个回调函数做为参数,该回调函数会在浏览器下一次重绘以前执行。————《MDN Web Docs》
就是在用这个能够一直重绘动画,而后让人看起来是个动画,重绘的这个过程是个很频繁的操做,因此若是咱们本身写,不加以干涉,在性能和资源上会形成严重的浪费,因此咱们可使用requestAnimationFrame来使用咱们的动画看起来很流畅,又不会频繁调用
优势
缺点
直接上图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>rAF使用</title>
<style> #SomeElementYouWantToAnimate { width: 100px; height: 100px; background-color: #000; } </style>
</head>
<body>
<div id="SomeElementYouWantToAnimate"></div>
<script> var start = null; var element = document.getElementById('SomeElementYouWantToAnimate'); element.style.position = 'absolute'; /** * @method 移动咱们的小黑方块 */ function step(timestamp) { if (!start) start = timestamp; var progress = timestamp - start; element.style.left = Math.min(progress / 10, 200) + 'px'; if (progress < 2000) { window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); </script>
</body>
</html>
复制代码
rAF是一个内部api,固定的16毫秒执行一次,由于人眼接受60fps的动画就会感到很流畅了,若是咱们须要改变rAF的执行时间,那咱们只能本身去写动画的方法,节流仍是防抖,看我的爱好了
防抖:连续触发一个函数,不论是触发开始执行仍是结束执行,只要在连续触发,就只执行一次
节流:规定时间内只执行一次,不论是规定时间内被触发了多少次
rAF:也算是一种节流手段,原生api,旨在使动画在尽可能少占用资源的状况下使动画流畅
lodash中相对应的_.throttle和_.debounce,在我看来是最佳实践了,推荐使用
《复仇者联盟4》现阶段的漫威宇宙的结束,《哈利·波特》《火影忍者》一个个完结的电影,虽然在时刻提醒着咱们青春再慢慢的消失,正如英雄联盟中的那句话,咱们有了新的敌人叫“生活”。当这些完结的并非真正的结束,《哈利·波特》有《神奇动物在哪里》,《火影忍者》有《博人传》,《钢铁侠》有《蜘蛛侠》,晚辈从前辈手中接过接力棒,继续日后跑,咱们也从本身青葱的岁月进入下一阶段,努力奋斗吧!!
我是前端战五渣,一个前端界的小学生。