阅读:
https://juejin.cn/post/7016502001911463950
https://juejin.cn/post/6844903480239325191
https://juejin.cn/post/6844903481761857543
https://juejin.cn/post/7040633388625035272
1 防抖
防抖的原理
就是:(连续触发一个事件,间隔时间没有超过n秒,都只执行一次) 多次触发事件,在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,那就以新的事件的时间为准,n 秒后才执行,总之,就是要等触发完事件 n 秒内不再触发事件,才执行。
【关注的是间隔时间内"最后一次"操作后的结果反馈】
防抖常见情景:
- 监听滚动条滚动(window 的scroll)(例如监听滚动条滚动要执行一些业务操作);
- 监听浏览器窗口变化(window 的 resize)(例如在 echarts 的应用中,默认浏览器窗口大小改变 echarts 视图布局是不会做响应式改变的,那么就需要通过监听浏览器窗口大小改变然后去重置 echarts 实现布局的改变。);
- 表单输入的一些监听事件,例如 oninput 等(例如做表单输入校验时,连续的输入几个字可能会触发多次校验,防抖可以做到一段时间内连续输入多个文字只校验一次);
- 鼠标事件,例如mousedown、mousemove(例如拖拽等的监听等,出于准确性和及时性, 他们的监听响应十分细密,而当这种频繁在业务上可能不是必要的,那么也可以考虑使用防抖动技术);
- 键盘事件,例如keyup、keydown
△模板:
// 函数防抖的实现 -- 无immediate版
function debounce(fn, wait) {
let timer;
return function() {
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) clearTimeout(timer);
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
}
/**
* 1.apply改变this指向
* 2.args--传入的func的参数对象
* 3.result--传入的func的返回值--问题:immediate不为true拿不到result,result在setTimeout里面了。所以呢???咋拿到呀待定
* */
function debounce(func, wait, immediate) {
let timeout;
//let result;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
let callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
// if (callNow) result = func.apply(context, args)
// return result;
}
else {
timeout = setTimeout(function(){
result = func.apply(context, args)
}, wait);
}
// return result;
//问题:immediate为false拿不到result,result在setTimeout里面了,异步赋值。会先执行return result。
}
}
- 疑问1: 下面2种写法,2个this的位置区别?
答:
第1种 – // 定时器setTimeout是Window定义的,setTimeout(function(){console.log(this)},wait)
的this指向Window,∴需要使用apply改变this指向为context。
第2种 – 使用了箭头函数(ES6箭头函数里this的指向就是上下文里对象this指向,偶尔没有上下文对象,this就指向window
),和第1种的this指向的位置是一样滴
①
②
- 疑问2:不是每次滚动都执行debounce函数吗,但是滚动多次只打印一次timeout
?
答:页面滚动执行的函数是debounce函数里面return的函数
function debounce(func, wait, immediate) {
var timeout, result;
console.log(timeout,'timeout')
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
result = func.apply(context, args)
}, wait);
}
return result;
}
};
function handle(){
console.log('handle')
};
// 滚动事件
window.addEventListener('scroll', debounce(handle, 500));
- 疑问3: 为什么setTimeout里面执行的func函数的this指向改变为context位置的this呢, 比如说,context在这个位置就赋值this不行吗(蓝色标记)
?
答: 外部的this的上下文指向的是debounce函数的上下文,闭包函数内部的this指向的上下文是实际函数执行的上下文,∴当然不能把context放到这个位置赋值this。
再看下这个 闭包和this指向问题
- 疑问3.2:那为什么谷歌浏览器中实现的效果是一样的呢?为什么func要使用apply?不使用apply直接执行func也可以实现效果?
答:
蓝色和红色位置this指向刚好一样而已,谷歌浏览器中测试因为蓝色标记位置此时的this指向Window;
可以放到Vue文件里面测试看看,蓝色位置是debounce函数执行上下文,指向Vue,红色位置默认指向Window。
谷歌浏览器中测试结果:
Vue文件中测试结果(这个测试例子很奇怪,体现不了apply的作用其实。。因为debounce函数在methods里面定义,context0是指向Vue实例;而context默认指向Window;这样子传入this.handle无论有无apply都无所谓,因为this.handle传入的时候是本来就是指向Vue实例,是可以实现防抖效果,无关apply的事。不过一般防抖函数是定义在其他文件,被引入进来的,而不是定义在组件methods里面,这个例子不具代表性,继续看看有代表性的例子叭~↓):
<template>
<div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
<div>test</div>
</div>
</template>
<script>
export default {
name: 'Test',
methods: {
debounce(func, wait, immediate) {
var timeout, result;
var context0 = this;
console.log(context0, 'context0');
return function () {
var context = this;
var args = arguments;
console.log(context, 'context');
console.log(context0 == context, 'context0=context');
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(function () {
result = func.apply(context, args);
}, wait);
}
return result;
};
},
handle() {
console.log(this, 'handle--this');
},
},
mounted() {
// 滚动事件
window.addEventListener('scroll', this.debounce(this.handle, 2000));
},
};
</script>
- 疑问3.3具有代表性的体现apply作用的例子:
答:
① 新建一个js文件Test.js:
export function debounce(func, wait, immediate) {
var timeout, result;
var context0 = this;
console.log(context0, 'context0');
return function () {
var context = this;
var args = arguments;
console.log(context, 'context');
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(function () {
result = func.apply(context, args);
// func();
}, wait);
}
return result;
};
}
②新建一个Vue文件:Test.vue
<template>
<div>
<div @click="testClick">test</div>
</div>
</template>
<script>
import { debounce } from './test';
export default {
name: 'Test',
data() {
return {
a: 1,
};
},
methods: {
handle() {
console.log(this.a, 'handle--this');
},
testClick: debounce(function () {
//如果不使用apply改变this指向,这里的this是undefined;
// 使用apply改变this指向后,这里的this是指向Vue实例
console.log(this, 'thisthis');
this.handle();
}, 2000),
},
};
</script>
③测试结果:先输出context0->点击div,输出context,2s后输出handle-this结果
![在这里插入图片描述](https://img-blog.csdnimg.cn/d076c153db54485b936e56ead5afbf7e.png
为什么还没点击就输出content0?
testClick绑定的是debounce执行的返回值
-
疑问4:immediate版意义
答:immediate为true时,一点击按钮立即执行handle函数,wait时间内再次点击按钮不再执行handle,间隔时间大于wait会再次立即执行handle。 -
疑问5:immediate版为什么需要提前定义callNow变量、把timeout=置前?这样把timeout置前不行吗?
if (immediate) {
if (!timeout) result = func.apply(context, args);
timeout = setTimeout(function () {
timeout = null;
}, wait);
}
答:我的理解是让时间计算更精准一点。
如果改成上面那样timeout置后,用户点击按钮立即执行func,当执行完func之后才开始计算wait时间,下一次的时间间隔时间实际上是:执行func的时间+wait时间。
把timeout置前,间隔时间是:开始执行func到下次开始执行func时间间隔,这个时间更准确。
2 节流
节流的原理
很简单:如果你持续触发事件,每隔一段时间,只执行一次事件。
【关注的是操作过程中每隔一段时间的(持续的)反馈】
节流常见情景
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如判断是否滑到底部自动加载更多,用throttle来判断(单位时间内只判断一次)
△模板代码:
function throttle(func, wait) {
let timeout;
return function() {
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
}, wait)
func.apply(this, arguments)
}
}
}
- 使用时间戳【只有头】
(触发事件时立即执行,以后每过wait秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行)
// 第一版
function throttle(func, wait) {
let previous = 0;//这里是不是Date.now没关系,这儿绑定时就会执行
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous >= wait) {
func.apply(context, args);
previous = now;
}
}
}
- 使用定时器
①【只有尾】
(第一次触发时不会执行,而是在wait秒之后才执行,当最后一次停止触发后,还会再执行一次函数。)
// 第二版
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
②【只有头】
△其实一般节流需求只有一端就好了,记这个为模版吧
function throttle(func, wait) {
let timeout;
return function() {
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
}, wait)
func.apply(this, arguments)
}
}
}
- 时间戳&定时器(
有头有尾
)
// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = 0;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}
-
疑问1:清除定时器为什么不需要这样写?
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
理解:首先,第一次操作走进remaining<=0,立即执行 [ 这是头 ]。其次,之后的每一次操作如果都在间隔时间中间 没到时间,都会走进timer=setTimeout,这个是肯定不需要的,接着直接clearTimeout就好。在等到最后一次操作在间隔时间中间时,此时没有下次操作了就不会clearTimeout,因此,最后一次操作会在delay秒之后执行 [ 这是尾 ]。 -
疑问2:这样写行不行
理解:俺觉得可以,没看出啥问题…
function throttle2(fn, delay) {
let timer = null;
let prev = 0;
return function () {
let now = Date.now();
let remaining = delay - (now - prev);
const args = arguments;
// 头 -- 只是第一次触发执行一次(立即执行)
if (prev == 0 && remaining <= 0) {
fn.apply(this, args);
prev = Date.now();
}
// 第一次触发开始,都走这儿(延后执行)
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, delay);
}
}
}