原文链接: js 防抖节流和rAF
上一篇: 理解和使用 ES6 中的 Symbol
下一篇: vue-fun-api 使用
防抖: 在持续触发某个事件时, 只会在持续间隔大于delay的时候触发一次
节流: 在持续触发某个事件的时候, 按照给定的间隔真正执行函数
rAF: 浏览器js动画api
https://zhuanlan.zhihu.com/p/51608574
1.1 什么是防抖
在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时。
1.2 应用场景
(1) 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询 ajax 请求,这样可以有效减少请求次数,节约请求资源;
(2) window 的 resize、scroll 事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="text" id="input">
</body>
<script>
function debounce(fun, delay) {
return function (args) {
//获取函数的作用域和变量
let that = this
let _args = args
//每次事件被触发,都会清除当前的timer,然后重写设置超时调用
clearTimeout(fun.id)
fun.id = setTimeout(function () {
fun.call(that, _args)
}, delay)
}
}
function ajax() {
console.log('ajax')
}
let input = document.getElementById('input')
input.addEventListener('keyup', debounce(ajax, 1000))
</script>
</html>
代码说明:
1. 每一次事件被触发,都会清除当前的 timer 然后重新设置超时调用,即重新计时。 这就会导致每一次高频事件都会取消前一次的超时调用,导致事件处理程序不能被触发;
2. 只有当高频事件停止,最后一次事件触发的超时调用才能在 delay 时间后执行;
效果:
加入防抖后,当持续在输入框里输入时,并不会发送请求,只有当在指定时间间隔内没有再输入时,才会发送请求。如果先停止输入,但是在指定间隔内又输入,会重新触发计时。
2. 节流(throttle)
2.1 什么是节流
规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
2.2 应用场景
(1) 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
(2) 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据;
(3) 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>加入节流</title>
<style type="text/css"></style>
<script type="text/javascript">
window.onload = function () {
//模拟ajax请求
function ajax(content) {
console.log('ajax request ' + content)
}
function throttle(fun, delay) {
let last, deferTimer
return function (args) {
let that = this;
let _args = arguments
let now = +new Date();
if (last && now < last + delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fun.apply(that, _args);
}, delay)
} else {
last = now;
fun.apply(that, _args);
}
}
}
let throttleAjax = throttle(ajax, 1000)
let inputThrottle = document.getElementById('throttle')
inputThrottle.addEventListener('keyup', function (e) {
throttleAjax(e.target.value)
})
}
</script>
</head>
<body>
<div>
3.加入节流后的输入:
<input type="text" name="throttle" id="throttle">
</div>
</body>
</html>
效果:实验可发现在持续输入时,会照代码中的设定,每 1 秒执行一次 ajax 请求
3. 小结
总结下防抖和节流的区别:
-- 效果:
函数防抖是某一段时间内只执行一次;而函数节流是间隔时间执行,不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。
-- 原理:
防抖是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,都会清除当前的 timer 然后重新设置超时调用,即重新计时。这样一来,只有最后一次操作能被触发。
节流是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。
连续触发结束时执行,而我们现在说的 “前摇” 则是下面这种情况
在连续触发的一开始就执行了,然后往后的连续触发不执行,连续触发停止后再经过延时时间后触发才会再次执行
下面是我自己写的,大概意思是这样,代码实现也贴出来
<!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>
上面代码要是看不太明白,可以直接粘下去自己执行以下看看是什么感觉,就知道是什么意思了。
代码纯我自己写的,要是有不对的地方,请大佬指正啊
Throttle 节流
什么是节流
节流呢,也是我自己的理解,在连续触发一个方法的某一时间段中,控制方法的执行次数。
同样举个例子吧,一个地铁进站闸口,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
如果我们用 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)
诶??rAF 是什么?什么是 requestAnimationFrame?这在我没有写这篇博客的时候,我根本不知道 window 下还有个这个方法,神奇吧,那这个方法是干什么的呢??
告诉浏览器 —— 你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。————《MDN Web Docs》
就是在用这个可以一直重绘动画,然后让人看起来是个动画,重绘的这个过程是个很频繁的操作,所以如果我们自己写,不加以干涉,在性能和资源上会造成严重的浪费,所以我们可以使用 requestAnimationFrame 来使用我们的动画看起来很流畅,又不会频繁调用
优点
- 目标是 60fps(16 毫秒的一帧),浏览器将决定如何安排渲染的最佳时间。
- 相对简单和标准的 API,未来不会改变,减少维护成本。
缺点
- rAF 是内部 api,所以我们并不方便修改
- 如果浏览器选项卡没有激活,就用不了
- 兼容性不好,在 IE9,Opera Mini 和旧 Android 中仍然不支持
- 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>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,旨在使动画在尽量少占用资源的情况下使动画流畅