防抖和节流
<div id="box" style="width:300px;height:300px;background: #ccc;text-align: center;line-height: 300px;color:red;font-size: 40px;">0</div>
<script>
let box = document.getElementById("box");
let num = 0;
function count() {
box.innerHTML = ++num;
}
box.onmousemove = count;
</script>
div元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。
在前端开发的过程中,经常会需要绑定一些持续触发的事件,如 resize、onscroll、onmousemove、oninput、onkeypress等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
通常这种情况下我们怎么去解决的呢?
一般来讲,防抖和节流是比较好的解决方案。
函数的防抖和节流: 降低高频率的事件执行函数;提高性能;
1.防抖
防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
防抖函数分为非立即执行版和立即执行版。
非立即执行版
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
let box = document.getElementById("box");
let num = 0;
function count() {
box.innerHTML = ++num;
}
//box的onmousemove绑定了debounce的返回值;
box.onmousemove = debounce(count,1000);
//非立即执行版
function debounce(fn,time) {
let timer;
return function () {
if(timer){clearTimeout(timer)};
timer = setTimeout(()=>{
fn();
},time)
}
}
在触发事件后函数 1 秒后才执行,如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。
上述防抖函数的代码还需要注意的是 this 和 参数的传递:
function debounce(fn,time) {
let timer;
return function () {
let context = this;
let args = arguments;
console.log("args",args) //args是事件对象
if(timer){clearTimeout(timer)};
timer = setTimeout(()=>{
//fn();
fn.apply(context,args);
//apply用来改变fn中this指向
},time)
}
}
防抖函数的代码使用这两行代码来获取this和参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接收到事件对象 e 参数。
立即执行版
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
function debounce(fn,time) {
let timer;
return function () {
let context = this;
let arg = arguments;
if(timer){clearTimeout(timer)};
let callNow = !timer;
timer = setTimeout(()=>{
timer=null;
},time)
if(callNow){fn.apply(context,arg)};
}
}
双剑合璧版
也可以将非立即执行版和立即执行版的防抖函数结合起来,实现双剑合璧版的防抖函数。
let box = document.getElementById("box");
let num = 0;
function count() {
box.innerHTML = ++num;
}
box.onmousemove = debounce(count,1000,true);
function debounce(fn,time,immediate) {
let timer;
return function () {
let context = this;
let arg = arguments;
if(timer){clearTimeout(timer)};
if(immediate){
let callNow = !timer;
timer = setTimeout(()=>{
timer=null;
},time)
if(callNow){fn.apply(context,arg)};
}else{
timer = setTimeout(()=>{
fn.apply(context,arg);
},time)
}
}
}
防抖的应用场景:搜索功能;
场景:
假设我们网站有个搜索框,用户输入文本会自动匹配出一些搜索结果供用户选择。我们首先想到的就是监听keypress事件,然后异步去查询结果。
这个方法本身是没错的,但是如果用户每输入一个字符就触发一次请求,这明显不是我们想要的;
我们想要的是用户停止输入的时候才去触发查询的请求,这时候函数防抖可以帮到我们。
2.节流
节流就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
节流,一般有两种实现方式,分别是时间戳版和定时器版。
时间戳版
let box = document.getElementById("box");
let num = 0;
function count() {
box.innerHTML = ++num;
}
box.onmousemove = throttle(count,1000);
function throttle(fn,wait) {
let previous = 0;
return function () {
let context = this;
let arg = arguments;
let now = Date.now();
//每隔一段时间执行一次;
if(now-previous>wait){
fn.apply(context,arg);
previous=now;
}
}
}
在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
定时器版
function throttle(fn,wait) {
let timer;
return function () {
let context = this;
let arg = arguments;
if(!timer){
timer=setTimeout(()=>{
timer=null;
fn.apply(context,arg);
},wait)
}
}
}
在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。
时间戳版和定时器版的节流函数的区别:时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。
同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。
双剑合璧版
function throttle(fn,wait,type) {
if(type===1){
let previous = 0;
}else{
let timer;
}
return function () {
let context = this;
let arg = arguments;
if(type===1){
let now = Date.now();
//每隔一段时间执行一次;
if(now-previous>wait){
fn.apply(context,arg);
previous=now;
}
}else if(type===2){
if(!timer){
timer=setTimeout(()=>{
timer=null;
fn.apply(context,arg);
},wait)
}
}
}
}
节流的应用场景:上拉加载;
应用场景:
滚动浏览器滚动条时,更新页面上的某些内容或者去调用后台的某接口查询内容。
如果不对函数调用的频率加以限制的话,那么我们每滚动一次滚动条就会产生一次的调用,很消耗性能;
但是这跟防抖有所不同,我们不是要在每完成等待某个时间后去执行某函数,而是要每间隔某个时间去执行某函数,避免函数的过多执行。