一、使用背景
由于JavaScript是事件驱动的,大量操作会触发事件,加入到事件队列中处理,而频繁触发事件会导致性能的损耗,页面也有可能出现卡顿
例如常见的文本框输入,当用户在文本框输入搜索条件时,自动查询匹配的关键字并提示给用户,绑定input事件,当输入框内容发生变化时触发多次事件,这时候通过该事件调用接口返回查询数据时也会由于事件的多次触发而发送多次请求,针对这一类现象,更好的处理方法就是接下来说到的防抖与节流。
二、防抖节流的应用场景
1、防抖
多次触发事件,当触发停下来一段时间再执行。
应用场景:
-
搜索框输入关键字
-
频繁的点击按钮
-
监听浏览器滚动事件
2、节流
持续触发事件,但只按照规定的频率执行,会稀释事件触发的频率。
应用场景:
-
监听页面的滚动事件
-
鼠标移动事件
-
游戏中的一些设计
-
用户缩放浏览器的resize事件
ps:以上可以看出防抖节流的应用场景有很多相似的地方,这也是容易混淆它们的原因,在实际开发中需要根据需求的不同并结合两者的差别来选择最适合的处理方式
三、防抖节流方法的实现
现在像lodash、underscore等这些第三方库都有防抖节流方法的实现,用起来很方便,但是理解它们的原理同样也是很重要的。
1、防抖方法的实现
在函数持续多次执行时,等它冷静下来再执行,也就是说当持续触发事件时,函数不执行,等最后一次触发结束后过了一段时间,再执行
(1)思路分解
-
持续触发不执行
-
不触发的一段时间后再执行
(2)代码实现
function debounce(fn, delay) {
// 使用闭包来保存定时器的状态
var timer = null
return function() {
// 保存this和arguments
var _this = this
var _arguments = arguments
// 持续触发不执行
if (timer) {
clearTimeout(timer)
}
// 不触发一段时间后再执行
timer = setTimeout(function() {
// 改变this指向并传入参数
fn.apply(_this, _arguments)
}, delay)
}
}
(3)以上是最基础的防抖函数的写法,也是非立即执行版的防抖,下面是立即执行版的防抖,在触发事件后函数会立即执行,之后连续触发不执行,直到最后一次不触发后过一段时间才执行,简单来说就是在第一次触发就执行,之后遵循上面的执行顺序
优化1:
function debounce(fn, delay) {
var timer
return function() {
var _this = this
var _arguments = arguments
if (timer) {
// 有定时器就清除
clearTimeout(timer)
} else {
// 没定时器就立即执行
fn.apply(_this, _arguments)
}
timer = setTimeout(function() {
// 执行定时器内部代码时将定时器置空,假设也是第一次触发事件,实现立即执行的作用
timer = null
fn.apply(_this, _arguments)
}, delay)
}
}
优化2:
function debounce(fn, delay) {
var timer = null
return function() {
var _this = this
var _arguments = arguments
// 定义isInvoke默认为false
var isInvoke = false
if(timer) {
clearTimeout(timer)
} else {
console.log('xxx');
// 立即执行fn后把isInvoke置为true,让定时器里面的fn不再重复执行
fn.apply(_this, _arguments)
isInvoke = true
}
timer = setTimeout(function() {
timer = null
// 当isInvoke为false时才执行
if(!isInvoke) {
console.log('yyy');
fn.apply(_this, _arguments)
}
}, delay)
}
ps:
优化1与优化2在多次触发事件时执行次数没差别,但只触发一次时,优化1的触发事件会执行两次,优化2只会执行一次,isInvoke的作用就体现在这儿,当事件只触发一次时,fn已经立即执行过了,定时器里面的fn就不需要再执行了,isInvoke作为标识来决定fn的执行次数
(4)将两种版本结合,使用isNow作为传参用于判断是否是立即执行
founction debounce(fn, delay, isNow) {
var timer
return function() {
var _this = this
var _arguments = arguments
var isInvoke = false
if (timer) {
clearTimeout(timer)
}
if(isNow) {
// 立即执行版
if(!timer) {
fn.apply(_this, _argument)
isInvoke = true
}
timer = setTimeout(function() {
timer = null
if(!isInvoke) {
fn.apply(_this, _arguments)
}
}, delay)
} else {
// 非立即执行版
timer = setTimeout(function() {
fn.apply(_this, _arguments)
}, delay)
}
}
}
2、节流方法的实现
在连续高频触发事件时,让函数有节制的执行,不是触发一次就执行一次,而是在一段时间内只执行一次,来降低函数执行的频率
(1)思路分解
-
持续触发但不执行多次
-
到一定时间再执行
(2)代码实现
-
时间戳版(立即执行版)
function throttle(fn, wait) {
var last = 0
return function() {
var _this = this
var _arguments = arguments
// 记录此时的时间
var now = new Date().getTime()
if(now - last > wait) {
fn.apply(_this, _arguments)
// 这步是为了让下次事件触发的时间与这次的时间作比较
last = now
}
}
}
-
定时器版1(run类似一个开关,为true时触发事件可以通行,为false时触发事件不能通行)(立即执行版)
function throttle(fn, wait) { var run = true return function() { var _this = this var _arguments = arguments // 当run为false时说明定时器内部函数还没执行,此时经过的的事件触发不执行,函数也不再往下执行 if (!run) { return } // run=false来阻止后面的触发事件进入 run = false setTimeout(function() { fn.apply(_this, _arguments) // 当前执行完后将run置为true来让后面的事件通行 run = true }, wait) } }
3、定时器版2(与定时器版1原理相同,只不过这个是用定时器来标识是否到了让下一个事件通行的时间)
function throttle(fn, wait) { var timer return function() { var _this = this var _arguments = arguments if(!timer) { timer = setTimeout(function() { timer = null fn.apply(_this, _arguments) }) } } }
4、时间戳与定时器合体版(既实现立即执行,也实现最后一次执行)
function throttle(fn, wait) { var last = 0; var timer = null; return function() { // this和argument var _this = this; var _arguments = arguments; var now = new Date().getTime(); if (now - last > wait) { // 间隔事件大于wait则清除开启的定时器,执行fn if (timer) { clearTimeout(timer); // 将timer置为null为了下次事件触发间隔不超过wait时开启一个新的定时器 timer = null; } console.log('时间戳'); fn.apply(_this, _arguments); last = now; } else if (timer === null) { // 每次间隔不超过wait时开启一个新的定时器,在之后触发事件的间隔超过wait时将开启的定时器清除, // 只有当一次触发事件不满足间隔wait并且在这个间隔内没有再触发事件时才执行定时器里的fn timer = setTimeout(function() { timer = null; console.log('定时器'); fn.apply(_this, _arguments); }, wait); } } }
注意点:
在以上几个函数内部,return前有提前声明几个变量,以定时器版节流这个函数为例,每次触发事件时都会执行throttle函数,按理说var run = true也会被执行多次,也就是每次都会把run赋值为true,每次都能开启新的定时器,就达不到我们想要的效果了,但是,实际情况并非如此。分步说明函数执行过程:
-
首先函数throttle执行的结果是return后的function调用,也就是不断触发事件时执行的函数是它,而var run = true只会声明一次,不会被持续执行(可以通过打印结果看出)
-
外层的throttle只是提供了一个作用域,也就是这个作用域里定义的run,由于被return返回的函数内部一直在使用,从而形成闭包,使得run这个变量一直存在没有被销毁,正是利用这一点,用run作为一个开关来决定是否开启新的定时器
补充:
作用域:
-
作用域为代码执行开辟栈内存,也就是代码的执行环境,执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中
-
函数执行会形成一个私有作用域,打开一个页面会形成一个全局作用域
作用域链:
-
嵌套的函数形成不同层级的作用域,内部函数对外部函数变量的引用形成作用域链——作用域链的形成和复杂程度都与函数嵌套有关
-
内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数
-
(1)作用域链的最前端,始终都是当前执行的代码所在环境的变量对象,
(2)下一个对象来自于外部环境,再下一个对象来自再下一个外部环境,直到全局执行环境,
(3)作用域链的最后端,始终都是全局执行环境的变量对象
闭包:
(1)外层函数的局部变量被内层函数引用
——根据垃圾回收机制,外层函数的变量被内层函数使用,不会被系统回收,会一直保存在内存中(缺点是可能会造成内存泄漏)
(2)调用外层函数返回内层函数
借用某位大神的一段解释收个尾~
最后:
借鉴了一些前辈的博客再加上自己的理解写出的总结,有错误请随时指出~