说明
今天被问到了节流防抖技术,由于以前在项目中没有用到过,所以一脸蒙蔽;不百度都不知道是什么;
思路
其实问题的本质,就是多次触发后如何一次性调用的问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
输入框 : <input id="ipt" type="text">
</body>
<script>
var ipt = document.getElementById("ipt");
function test() {
console.log("dd")
}
ipt.addEventListener("keyup", function () {
var time = null;
if (time !== null) {
clearTimeout(time);
}
setTimeout(() => {
test();
}, 500);
})
</script>
</html>
这边呢,当时临时百度了下,就把答案写了上去,
晚上测试了下,发现其实还是调用两次,只不过是延时调用了;当时心里也是蛮抵触这种写法的,因为本身就比较讨厌settimeout这个回调函数。
现在有时间想了,大概的思路有以下几种:
首先呢,需求就是期望不要每次键盘抬起的时候都去调用这个函数,而是期望在某段时间,去调用,就是把结果一次性传递给后台。所以呢,像我这样些,还是有点问题的。这种场景其实用到的地方还是蛮多的,也还是值得研究的。可以优化掉多次请求的问题。
解决办法
在解决这个问题之前,我们需要弄清楚setTimeout,setInterval,clearInterval等的作用;
let time1 = setTimeout(() => {
}, 0); //time1 == 1
let time2 = setInterval(() => {
}, 20); //time2 == 2
clearTimeout(time2);
//我把这个代码放这里的作用呢,其实就是想告诉大家,返回值是公用的从1开始到无穷的数值,就是有几个定时器,就会返回几,而清除可以随便混用,都有效的。
1 通过规定的时间,去触发函数
var ipt = document.getElementById("ipt");
function test() {
console.log("dd")
}
var time=null;
ipt.addEventListener("keyup", function (e) {
if(time!==null){
clearTimeout(time);
}
time = setTimeout(() => {
test();
}, 500);
})
这里就比较简单了,当我们在键盘上输入1的时候,这时time是null,所以会执行定时函数,500ms后调用test();然后我们在按下2的时候,time==1,所以会清除掉前面的定时函数,time就被重新定义了,为2;下面的截图和我预想的一样,而且为了不过多的占用内存,我们最好初始化time;
最终的写法
var ipt = document.getElementById("ipt");
function test() {
console.log("dd")
}
var time=null;
ipt.addEventListener("keyup", function (e) {
if(time !== null){
clearTimeout(time);
}
time = setTimeout(() => {
test();
time = null;
}, 2000);
})
但是这里呢,需要写一个函数,封装成通用的,那这里就考虑写成如下:
思考的过程:
首先,我们已经面向过程的实现了;那么,我们要做的就是把已经实现的封装成一个函数,做成通用的
考虑到通用性,我们肯定是用函数声明的方式,不能用匿名函数,当然,函数表达式也可以
首先,我们要有一个变量time,相当于两层函数,第一层函数,声明一个变量time,第二个函数,做事件的处理
######首先的想法#########
function debounced() {
var time = null;
function child() {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
test();
time = null;
}, 2000);
}
}
######修改一些形参等#########
function debounced(fn,wait) {
var time = null;
function child() {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
fn();
time = null;
}, wait);
}
}
######函数内部函数调用#########
function debounced(fn,wait) {
var time = null;
function child() {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
fn();
time = null;
}, wait);
}
child();
}
测试下:
var ipt = document.getElementById("ipt");
function test() {
console.log("dd")
}
ipt.addEventListener("keyup", function () {
debounced(test,2000)
})
function debounced(fn,wait) {
var time = null;
function child() {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
fn();
time = null;
}, wait);
}
child();
}
发现还是有问题,然后查看下代码
然后按照代码的逻辑,我们走一遍;
逻辑:首先,在键盘连续输入了12345,会触发键盘抬起的监听,当1抬起时,触发debounced函数,在函数内部,声明time=null,声明child函数,调用,此时child为null,调用时间延迟函数,调用fn,time=null;当第二次调用debounced时,time仍然为null,所以又触发了时间延迟函数,,原来问题出在这里,就是我的time一直为null,这里如果我总是先声明一个time,好像始终逃不脱Null的嫌疑,所以这里我不声明,然后去查了很多资料,也算是整理出了一个很完整的例子,但是有几点我认为还是必须要弄明白的:
1 addEventListener使用匿名函数和有名称的函数的区别
2 settimeout使用匿名函数和有名称函数的区别
3 通过1,2 ,我们需要弄清楚函数声明的方式
4 闭包的作用域链的问题
5 函数this指向的问题
突然我发现防抖这个试题,考察的点真的很多,需要相当的基本功啊。
那么我们一个问题一个问题的来解决:
1 函数声明的方式
//函数声明
function add(){
console.log("函数声明")
}
//函数表达式
var test = function(){
console.log("函数表达式")
}
//实例化,这种用的很少
var te = new Function();
2 onclick和addeventlistener的区别以及addeventlistener里面传函数的区别
//函数声明
function add(){
console.log("函数声明")
}
//函数表达式
var test = function(){
console.log("函数表达式")
}
var ipt = document.getElementById("ipt");
ipt.onclick=function(){
console.log("aa")
}
ipt.onclick=test;
ipt.onclick=add;
通过上面的对比,我们发现onclick只能绑定一个事件
这里我把onclick直接执行
//函数声明
function add(){
console.log("函数")
}
//函数表达式
var test = function(){
console.log("函数表达式")
}
var ipt = document.getElementById("ipt");
ipt.onclick=add();
我发现点多少次都没有反应了,而且是一开始就执行了,这里应该也不难理解,
然后我们来看看addeventlistener
//函数声明
function add(){
console.log("函数")
}
//函数表达式
var test = function(){
console.log("函数表达式")
}
var ipt = document.getElementById("ipt");
ipt.addEventListener("click",function(){
console.log("1")
})
ipt.addEventListener("click",test)
显然这里的addeventlistener可以绑定多个
settimeout传入函数的区别
//函数声明
function add(){
console.log("函数")
}
//函数表达式
var test = function(){
console.log("函数表达式")
}
var ipt = document.getElementById("ipt");
setTimeout(() => {
console.log("hehe")
}, 1000);
setTimeout(add, 2000);
setTimeout(test(), 5000);
基本上也是显而易见的
函数的作用域链及闭包的关系
1 首先作用域
全局作用域就是整个script标签内的
局部作用域就是函数内部的,也叫函数作用域
2 作用域链
函数内部套函数,就产生了作用域链,就近原则
3 闭包
闭包就是通过函数内部嵌套函数的形式,产生可持久化的局部变量,避免全局变量污染,又可以持久化操作变量
4 匿名函数的作用域
匿名函数是可以访问到全局变量的
好了,这几个问题搞明白了,花了我一天的时间,现在我们来正式的完成函数防抖的功能:
var ipt = document.getElementById("ipt");
var ipt1 = document.getElementById("ipt");
function ajax() { console.log("发送请求");console.log(this);console.log(arguments) }
// ipt.addEventListener("keyup", function () {
// y()
// })
//封装一个通用函数,在一定的时间内,多次请求只触发一次
//精简版(1)
function debounced(fn, wait) {
let timer = null;
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, wait);
}
/**迭代1,由于(1)中,每次调用这个函数,都会重新生成timer,所以清除
* 函数并没有把之前的timer清除掉,要想把之前的timer清除掉,必须弄一个
* 全局变量,但是每次调用该函数,都弄一个全局变量,会造成变量污染和内存溢出
* 不能这样做,但是我们先看看这样做的危害
* 危害:
* 1 产生全局变量污染 timer
* 2 不具有通用性,当有多个地方调用该函数时,比如我想点击时,延迟300ms发送
* ajax,在键盘抬起时1s发送请求,那么就会产生循环覆盖
*/
function debounced1(fn, wait) {
if (!window.timer) {
window.timer = null;
}
clearTimeout(window.timer);
window.timer = setTimeout(() => {
fn();
}, wait);
}
/**
* 迭代2,那么我们就要考虑抛弃全局变量的做法,由于封装的函数,既要有全局变量
* 的使用性质,又是希望给每个人单独使用的,那么不可避免的我们想到了闭包,闭包可以
* 产生局部的可持久化操作的变量,但是在这里有一个难点:
* ipt.addEventListener("keyup", function () {m();}) 大家看看我是这样调用的,而且我下面采用的
* 是函数表达式的情形,还立即执行了?
* 如果说,我单单这样写:
* function debounced2(fn, wait) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, wait);
}
}
然后这样去调用:
ipt.addEventListener("keyup", function () { debounced2(ajax,500)() })
这样做的逻辑流程就是:我每点击一次键盘,就会调用一次debounced2,然后返回function再次调用,相当于
每次我的timer还是新生成的,知道不;所以,要保证我的闭包函数,只能被调用一次才可以,那么只能像下面这样去写
也就是声明后,立即执行了去
*/
var m = function debounced2(fn, wait) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, wait);
}
}(ajax,500)
/**
* 迭代3 根据上面的情形呢,可以分成两种情况,一种就是,
* ipt.addEventListener("keyup", function () { })不变的情形,意思就是,我就想用这种回调的形式去操作;
* 那么就只能跟迭代2一样,先执行了
*/
var y= function debounced3(fn, wait) {
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, wait);
}
}(ajax,500)
console.log(y)
/**
* 迭代4 第二种情形呢,就是我们可以改变下调用的方式
* ipt.addEventListener("keyup",debounced4(ajax,500))
* 这里可能就要稍微解释下了,为什么这样去做就可以,首先我们看下函数说明
* addEventListener(event,function,useCapture);我们只看第二个参数哈,funciton,这是什么意思呢,如果
* 我们传入的是一个匿名函数,相当于就是一个回调函数,只有当事件event触发过后,才会去执行,那么每次执行,相当于
* 我们每次都要触发debounced4,所以我们的闭包函数每次都被重新触发了,那么在内存中,每次都开辟空间,产生了新的timer,
* 这样的话,我们的定时器也都是新的;但是,我们如果直接把闭包函数放到那里去,相当于开始就执行了一次闭包函数,在内存
* 空间只开辟了一个timer,闭包函数运行完,返回的是什么?是function(){clearTimeout(timer....},所以在不同的事件中
* 我们这样去使用,就相当于每个事件都开辟了自己的内存空间timer,然后返回闭包放到那里,当事件触发后,去执行。
*/
function debounced4(fn, wait) {
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, wait);
}
}
// ipt1.addEventListener("keyup",debounced4(ajax,500))
/**
* 迭代5,我们知道哈,我们的事件接触,在回调函数里啊,是可以拿到一个参数e的,也就是键盘触发事件的所有信息
* ipt1.addEventListener("keyup",function(e){
console.log(e)
})
那么,我们如果采用了上面的写法,怎么取拿到这个e呢?其实添加监听事件的函数,可以简化成
function test(callback){
var e="键盘的所有信息";
callback(e)
}
*/
//这里 var callback = function(){} ====> callback(e) ==== function(){}(e),所以就有了下面的方法
function debounced5(fn, wait) {
let timer = null;
return function(e){
clearTimeout(timer);
timer = setTimeout(() => {
fn();
console.log(e)
}, wait);
}
}
// ipt1.addEventListener("keyup",debounced4(ajax,500))
//这里大家看的可能还是有点不懂。。。那么请私信我。。因为中间删了一些推论过程
/**
* 迭代6,至此,防抖的功能基本实现了,而且也拿到了我们的键盘事件。 但是,我们知道,函数的this都是指向window的,
* 那么当我们返回后,这里事件结束时的this指向肯定变了;本来我们的this,是指向 <input id="ipt" type="text">
* 但是我们肯定希望ajax在被事件调用完后,只指向调用的dom的
*
*/
function debounced6(fn, wait) {
let timer = null;
return function(e){
clearTimeout(timer);
var that = this;
timer = setTimeout(function(){
fn.call(that);
}, wait);
}
}
//当然,这里也可以用到es6的语法
function debounced6(fn, wait) {
let timer = null;
return function(e){
clearTimeout(timer);
timer = setTimeout(()=>{
fn.call(this);
}, wait);
}
}
// ipt1.addEventListener("keyup",debounced6(ajax, 500))
/**
* 迭代7 然后呢,针对迭代5,我们还有更简单的方法,由于在javascript中,执行函数没有设置参数,也是可以被传入参数的,这句话
* 是什么意思呢,比如
* function test(){
* }
* 我没有设置参数,但是我还是有办法把参数穿进去,比如
* !function test(){
* console.log(arguments)
* }("123")
* 这里的参数我们就传进来了
*/
function debounced6(fn, wait) {
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(()=>{
console.log(arguments)
fn.call(this,arguments);
}, wait);
}
}
ipt1.addEventListener("keyup",debounced6(ajax, 500))
总结
里面涉及到了很多知识点,光看个结果是没有意义的;还是要多思考