div元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。
<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>
在前端开发的过程中,经常会需要绑定一些持续触发的事件,如 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 () {
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)
}
}
在触发事件后函数 1 秒后才执行,如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。 上述防抖函数的代码还需要注意的是 this 和 参数的传递:
let context = this;
let args = arguments;
防抖函数的代码使用这两行代码来获取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)
}
}
}
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)
}
}
}
}