对于像onresize、onscroll、onmousemove等高频率事件,频繁触发时重复调用回调逻辑,复杂的情况下会造成页面卡顿(通常浏览器每秒60帧, 一帧1/60=16.67ms,每帧的用时超过这个时间,页面就会卡顿)
//监听鼠标移动事件
document.onmousemove = function() {
count = count+1
dom.innerText = count
}
鼠标不经意间的移动,就会造成事件的多次调用,如果回调函数里的逻辑很复杂,就有可能造成页面的卡顿。但是对于用户来说,只要能保证页面流畅就好,高频的刷新也会浪费资源。

函数的防抖
当持续触发事件时, 如果在指定的时间间隔内没有再触发该事件,事件处理函数才会被执行;如果在设定的时间间隔内触发了该事件,该次事件会被忽略 事件间隔以当前事件发生时开始计算。根据实际需要可以分为立即执行版本和非立即执行版本。

实现思路就是利用定时器延迟执行,如果在指定时间段内再次触发事件,就清除上一个定时器,重新定义一个新的定时器执行函数;如果指定时间段内没有再触发事件,则该定时器函数才会执行。
简版的防抖函数
function debounce(fn, wait){
let timeout; //利用函数闭包及变量的作用域(定义时决定的)
return function() {
//如果指定时间段内,事件再次触发,则忽略掉(事件函数不执行)
if(timeout) clearTimeout(timeout)
//如果定时器函数没有被清除, 则说明指定时间段内还没有触发事件;如果超过指定时间段后,定时器依然没有被清除,则说明在该时间段内都没有再次触发事件,那么事件函数fn会被调用执行
timeout = setTimeout(fn, wait)
}
}
还是拿上面鼠标移动的onmousemove事件来实验:
document.onmousemove = debounce(fn, 300)
function fn() {
count = count+1
dom.innerText = count
}

防抖函数内部this
上面的简版防抖函数已经可以满足需求了, 但是原函数内部this指向发生了变化
//before
document.onmousemove = fn; // 注意后面没有括号, 这里只是变量引用
function fn() {
console.log(this) //该函数作为对象的方法,那么在调用时,函数内部this指向调用者即document;
}
}
//after
document.onmousemove = debounce(fn, 300) // 注意后面有括号, 这里是函数调用执行
function fn() {
console.log(this) //这里的函数fn作为普通函数被调用执行, 那么在浏览器环境的非严格模式下函数fn内部的this指向Window
}
这里该怎么改变函数fn内部的this指向呢? 此时我们可以很容易想到了call、apply、bind。但是我们要如何做在debounce函数中获得fn的this对象呢?从而将获取的对象作为call、apply、bind函数的第一个参数传入。
和变量不同函数内部的关键字this,是由运行时决定的, 并不是在定义是决定的(箭头函数除外)。也就是说函数在没有被调用执行之前,是不能够断定函数内部this指向的
document.onmousemove = debounce(fn, 300)
function debounce(fn, wait){
console.log(this) //同after,作为普通函数被调用执行,内部this指向window
let timeout;
return function() { //debounce函数运行后返回一个匿名函数,该匿名函数被document.onmousemove 引用,作为document对象的方法
console.log(this) //同before, 该函数作为对象的方法被调用执行,函数内部this指向调用者
const context = this
if(timeout) clearTimeout(timeout)
timeout = setTimeout(fn.bind(context), wait)
}
}
防抖函数事件对象
触发事件的回调函数中有时会用到事件对象event, 但是经防抖函数包装后,我们并没有给fn传递事件对象;跟上面的this一样我们如何获取到事件对象呢?
事件对象:在IE6/7/8中事件对象,事件对象作为全局对象
window的属性window.event存在 ,现在的大部分浏览器中,事件对象都以事件处理函数的第一个参数传入
function fn(event) {
console.log(event)
}
function debounce(fn, wait){
let timeout;
return function() { //事件对象作为第一个参数传入事件函数
const context = this
const args = arguments //通过arguments获取到参数列表
if(timeout) clearTimeout(timeout)
timeout = setTimeout(function() {
fn.apply(context, args) //通过apply立即执行函数,传入this和事件对象
}, wait)
}
}
防抖函数的立即执行
当我们我们需要立即执行的防抖函数,即在事件首次触发时,事件处理函数立即执行,指定时间内的事件触发,都将忽略调
function debounce(fn, wait){
let timeout;
return function() {
const context = this
const args = arguments
if(timeout) {
clearTimeout(timeout)
timeout = setTimeout(() => {
timeout = null
}, wait)
} else {
fn.apply(context, args) //首次触发立即执行
timeout = setTimeout(() => {
timeout = null
}, wait)
}
}
}
防抖函数的返回值及取消
function debounce(fun, wait) {
let timeout, result;
let debounced = function() { //将该函数赋值给一变量, 同时返回该变量
if(timeout) clearTimeout(timeout)
timeout = setTimeout(functio() {
result = fun() //接受事件处理函数返回值
}, wait)
return result;
}
debounced.cancel = function() { // 函数本身也是一个对象, 可以将取消的属性添加到该函数对象身上。
clearTimeout(timeout) //清除定时器
debounced = null //因为之前在定义定时器时返回的编号还在, 需要释放掉内存
}
return debounced;
}
函数的节流
另一种优化高频触发事件的方式就是节流函数。节流函数核心就是稀释函数的执行频率,对于连续触发的事件每隔一段时间最多只会触发一次事件执行。
节流函数有两种方式可以实现, 一种是定时器版本,一种是根据时间戳来判断
//定时器方式
function throttle(fun, wait) {
let timeout;
return function () {
var context = this;
var args = arguments;
if(!timeout) { // wait时间段内只会触发一次事件调用
fun.apply(context, args)
timeout = setTimeout(() => {
timeout = null //wait之后清除变量
}, wait);
}
}
}
高频事件第一次触发时会立即调用事件处理函数并执行,同时设置一个定时器,在指定时间wait后重置变量;如果在指定时间wait之内再有事件触发,由于之前设置的定时器timeout有定义,会忽略掉这次事件。
//时间戳
function throttle(fun, wait) {
let startTime = 0 ;
return function () {
var context = this;
var args = arguments;
let endTime = Date.now()
if(endTime - startTime > wait) {
startTime = endTime
fun.apply(context, args)
}
}
}
时间戳版的节流函数,根据事件发生前后的时间戳间隔来稀释事件触发频率。在每次事件函数调用时,重新记录当前时间,与后续的事件触发时间差值相比。如果大于指定时间段wait则说明进入下一个时间段,可以调用时间处理函数;如果小于指定时间段,则说明还在当前时间段内,不调用函数。
总结
事件节流是如果高频事件一直触发,那么每隔一段时间都会触发一次,而事件防抖则是忽略掉其他,只在最后一次调用事件处理函数(可以立即执行);
事件节流是每隔一段时间都会重计算事件周期,事件防抖则是只要触发事件,就会重新计算事件周期;