哈喽大家好,我是阿锋,趁着周末无聊的时候,写了一下防抖和节流。废话不多说直接上代码
防抖
什么是防抖
个人理解:防抖就是当你重复某一个动作的时候,实际只做一件事。
为什么需要防抖
在平时开发中。如果有一个input输入框或者点击按钮需要用户输入或者点击时发送网络请求。因为我们不能控制用户输入的次数,所以输入的时候可能是一直输入。我们在监听事件的时候会根据输入的东西来发送请求,那么就会一直发送请求。这样以来对服务器的压力是非常大的。更何况还是一个人,如果是每个人都这样的话,那估计服务器迟早要崩。所以我们前端在处理这个问题的时候就用上了防抖。
如何写出
那么我们就来写一个防抖。手写防抖之前,应该先看看没有防抖的效果,以及有防抖的效果。这里我们引入underscore
我们先看没有防抖的效果
首先写一个input标签
<input type='text></input>
var el = document.querySelector('input');
var index = 0;
var inputChange = function(){
console.log(`执行了${++index}次函数`);
}
el.oninput = inputChange;
运行在浏览器上查看效果
可以看到我输入了abc,执行了三次事件。 然后我们引入underscore的防抖
el.oninput = _.debounce(inputChange, 2000);
查看有防抖后的效果
可以看到,引入防抖后,多次输入,只执行了一次。这样以来,服务器的压力必定会小很多。
ok,看到了防抖的效果后,那么我们来手写一个属于自己的防抖
手写属于自己的防抖
分析
通过使用防抖效果分析。
- 运行的函数还是我们之前使用的函数
- 连续输入的时候,不会执行函数,当我输入一会儿不输入的时候才会执行
- 当我输入完之后需要等一会儿才执行
现在根据我们的分析一步一步来完成对防抖函数的编写
基本实现
首先,我们在使用别人写的防抖的时候,是将我们需要执行的函数传入到他们已经写好的函数里。其实这样想,是比较合理的,因为你不知道,别人会使用什么样的函数,写什么样的代码。所以最好用一个函数来接收调用者的函数。同时我们又传了一个时间,表示延迟多少秒。最后人家是返回的一个函数,至于为什么是返回一个函数,去看一下Dom,Bom吧。
function debounce(fn, delay){
var _debounce = function(){
fn();
}
return _debounce
}
上面代码执行
发现我们满足了分析的第一条,执行的是我们传入的函数,但是我们没有延迟。 随即我们加上延迟
function debounce(fn, delay){
const _debounce = function(){
setTimeout(() => {
fn();
}, delay)
}
return _debounce;
}
运行在浏览器上,发现虽然是有延迟但是却是每次输入的事件都有延迟。 再将代码更改一下
function debounce(fn, delay){
let timer = null;
const _debounce = function(){
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, delay)
}
return _debounce;
}
运行代码,发现多次输入时,停下等待一定的时间,才会执行我们的事件,并且只执行一次。 到现在我们完成了防抖函数的基本实现
this指向和参数问题
上面,我们完成了防抖函数的基本实现。但是,这个函数仍有一定的问题,比如;this的指向和参数问题。 我们将防抖函数去掉,用最基本的事件函数去查看this和参数看看。
const inputChange = function(event){
console.log(`执行了${++index}次函数`, this, event);
}
运行
可以看到,this是发生事件的元素,event是事件对象。
然后用引入js库的防抖函数查看
最后我们用自己写的查看
可以看到我们自己写的this纸箱window, event是undefined
这不对呀!!那我们改改代码吧!!
function debounce(fn, delay){
let timer = null;
const _debounce = function(...arg){
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arg);
}, delay)
}
return _debounce;
}
到此,this的问题和参数的问题就解决了
立即执行
在平时开发中,有些需求需要输入的时候立即发送网络请求。所以我们也需要做一个这种的需求 那么这个需求是可选的,因为不是所有的输入都需要立即执行,需要调用者自己来控制 所以我们这里写一个参数,让调用者自己控制
function debounce(fn, delay, immdiate = false){
let timer = null;
let isImmdiate = false;
const _debounce = function(...arg){
if (timer) clearTimeout(timer);
if (immdiate && !isImmdiate) {
fn.apply(this, arg)
isImmdiate = true;
}
timer = setTimeout(() => {
isImmdiate = false;
fn.apply(this, arg);
}, delay)
}
return _debounce;
}
为什么需要再弄一个变量来控制呢?因为我们在调用函数的时候尽量不要去修改传入的参数,最好自己定一个一个变量来控制
取消操作
有时候,用户输入了之后,他不想查询了或者他去到了其他的页面,那这个时间我们就没必要发网络请求。所以我们需要一个取消操作
这里需要前置知识: 函数、原型、对象三者的关系。
function debounce(fn, delay, immediate = false){
let timer = null;
let isInvoke = false;
const _debounce = function(...arg){
if (timer) clearTimeout(timer);
if (immediate && !isInvoke) {
fn.apply(this, arg);
isInvoke = true;
}
timer = setTimeout(() => {
fn.apply(this, arg);
isInvoke = false;
timer = null;
}, delay)
}
_debounce.cencal = function() {
if (timer) clearTimeout(timer);
timer = null;
isInvoke = false;
console.log('点击了取消')
}
return _debounce;
}
以上就是我对防抖函数的封装,其实还可以封装返回值,但是我没有遇到需要返回值的防抖。所以这里就不做这个了,有想法的小伙伴可以自己去封装一下。
节流
什么是节流?
个人理解:多次执行某一个动作,按照相同时间执行这个动作。
为什么需要节流
在平时开发中,有一个输入框,我们需要用户输入东西的时候按照一定的时间间隙来发送网络请求,此时,防抖显然是达不到我们的要求,所以我们需要用节流。
如何写出
我们先看没有节流的效果
<input type='text></input>
var el = document.querySelector('input');
var index = 0;
const inputChange = function(event){
console.log(`执行了${++index}次函数`, this, event);
}
el.oninput = inputChange;
运行在浏览器上,多次输入
查看有节流时的效果
手写属于自己节流
分析
- 执行的函数是我们自己的函数
- 当我多次输入的时候事件函数的执行会按照我规定的时间执行。
- 第一次输入的时候立即执行
- 当我输入完之后,到我规定时间后,事件函数又执行了一次。
基本实现
通过上面的分析,我们一步一步跟着分析来,首先是1和2。节流比防抖更难一点,这里需要我们画图进行分析
若我有一个事件函数传入,并且传入间隔时间是10s。那么我开始输入东西,在10s内函数都不会执行(排除立即执行和最后的函数执行)。从我第一次输入东西已经过去了1s,那么此时还有9s。输入第二次一共花了2s,还剩8s。一直持续下去, 7,6,5,4,3,2,1,0 .当我输入东西的时候到了10s的时候,此时就应该执行传入的事件函数,11s的时候进入下一个循环,以此类推。
那么,我们思考一下,应该如何拿到当前还有多少秒到达我们需要执行函数的时间呢?注意看我们上面的图。由图可见: 上一次执行时,我们可以设置为0,第二次执行时,我们可以获取当前时间,然后用我们需要等待时间减去他们之间的时间差,判断是否小于0不就可以了嘛?
function throttle(fn, interval){
// 设置上一次函数执行时间,由于是第一次执行,所以设置为0
let lastTime = 0;
// 真正执行的函数
var _throttle = function(){
// 拿到当前执行函数的时间
const newTime = new Date().getTime();
var raminTime = interval - (newTime - lastTime);
if (raminTime <= 0) {
fn();
// 将这次执行的时间给到上一次时间,以便于下次判断;
lastTime = newTime;
}
}
return _throttle;
}
查看效果
这里的效果有一个问题,输入第一个的时候立即执行了。因为是否立即执行应该是调用者控制的,所以我们需要修改一下
立即执行
这里说明一下,因为我们还要考虑到输完后没有达到规定间隔时间的时候的参数,所以这里多个参数看起来有点太长了,所以我们用对象比较好点。
再次回到我们上面的图:我们需要立即执行的时候,是我们每次间隔开始的第一时间就进行判断了。如果调用者传入的是true,那么我们就应该给个间隔时间中第一时间进行立即执行。同时就需要interval - (newTime - lastTime) 小于0,interval,newTime 我们知道,那么lastTime 不就等于newTime,所以我们修改代码。
function throttle(fn, interval, option = { immedia: true }){
// 设置上一次函数执行时间,由于是第一次执行,所以设置为0
let lastTime = 0;
let { immedia } = option;
// 真正执行的函数
var _throttle = function(){
// 拿到当前执行函数的时间
const newTime = new Date().getTime();
// 这里的判断是防止每次调用都会立即执行,可以复制测试
if (!lastTime && !immedia) lastTime = newTime;
var raminTime = interval - (newTime - lastTime);
if (raminTime <= 0) {
fn();
// 将这次执行的时间给到上一次时间,以便于下次判断;
lastTime = newTime;
}
}
return _throttle;
}
输入完之后没有到时间是否要执行一次
有这样一个需求,当我输入之后停止,还没有到达我们规定的间隔时间,在上面我们实现中是不会执行事件函数的,但是我想要他执行函数呢?因为你仍有输入东西,仍有改变input的值。
这个操作应该是需要调用者控制的,所以我们需要传值进来。需要明白的是,我们首先输入一次,没有再输入的时候,会执行一次,但是如果我们一直输入到我们规定间隔时间的时候,是会执行间隔时间里面的。这可能不太好理解,我们以代码来解释吧
function throttle(fn, interval, option = { immedia: true, trailing: false }){
// 设置上一次函数执行时间,由于是第一次执行,所以设置为0
let lastTime = 0;
let { immedia, trailing } = option;
let timer = null;
// 真正执行的函数
var _throttle = function(){
// 拿到当前执行函数的时间
const newTime = new Date().getTime();
// 这里的判断是防止每次调用都会立即执行,可以复制测试
if (!lastTime && !immedia) lastTime = newTime;
var raminTime = interval - (newTime - lastTime);
if (raminTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn();
// 将这次执行的时间给到上一次时间,以便于下次判断;
lastTime = newTime;
// 这里return 是防止后面的判断执行后再次调用事件函数
return;
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null;
lastTIme = !immedia? 0 : new Date().getTime();
fn();
// raminTime时间刚好是输入之后,距离执行到了间隔时间的时间。
}, raminTime)
}
}
return _throttle;
}
this指向和参数问题
this指向和参数问题是比较简单的,不过多阐述
function throttle(fn, interval, option = { immedia: true, trailing: false }){
// 设置上一次函数执行时间,由于是第一次执行,所以设置为0
let lastTime = 0;
let { immedia, trailing } = option;
let timer = null;
// 真正执行的函数
var _throttle = function(){
// 拿到当前执行函数的时间
const newTime = new Date().getTime();
// 这里的判断是防止每次调用都会立即执行,可以复制测试
if (!lastTime && !immedia) lastTime = newTime;
var raminTime = interval - (newTime - lastTime);
if (raminTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
// 改变this和传入参数
fn.apply(this, args);
// 将这次执行的时间给到上一次时间,以便于下次判断;
lastTime = newTime;
// 这里return 是防止后面的判断执行后再次调用事件函数
return;
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null;
lastTIme = !immedia? 0 : new Date().getTime();
// 改变this和传入参数
fn.apply(this, args);
// raminTime时间刚好是输入之后,距离执行到了间隔时间的时间。
}, raminTime)
}
}
return _throttle;
}
取消操作
取消操作和防抖的差不多
function throttle(fn, interval, option = { immedia: true, trailing: false }){
// 设置上一次函数执行时间,由于是第一次执行,所以设置为0
let lastTime = 0;
let { immedia, trailing } = option;
let timer = null;
// 真正执行的函数
var _throttle = function(){
// 拿到当前执行函数的时间
const newTime = new Date().getTime();
// 这里的判断是防止每次调用都会立即执行,可以复制测试
if (!lastTime && !immedia) lastTime = newTime;
var raminTime = interval - (newTime - lastTime);
if (raminTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
// 改变this和传入参数
fn.apply(this, args);
// 将这次执行的时间给到上一次时间,以便于下次判断;
lastTime = newTime;
// 这里return 是防止后面的判断执行后再次调用事件函数
return;
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null;
lastTIme = !immedia? 0 : new Date().getTime();
// 改变this和传入参数
fn.apply(this, args);
// raminTime时间刚好是输入之后,距离执行到了间隔时间的时间。
}, raminTime)
}
}
_throttle.cancel = function() {
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle;
}
总结
以上就是个人对防抖和节流函数的封装。当然还是封装的不够完善,比如返回值的获取。但是我们在开发中很少有这样的需求。上面所说若有错误欢迎指正。学习不仅要看,还要跟着写!加油!