开发中有一些页面的节点绑定的事件会被频繁触发,比如鼠标移动 onmousemove,窗口的resize,scroll,输入框的改变,我们希望他触发时有效果但又不希望被频繁的触发影响性能,这就是我们的初衷
先看个小案例:
<body>
<!-- 一个盒子 用于展示数字 -->
<div id='content'></div>
</body>
<script>
let count=1//计数变量
const dom =document.getElementById('content')
// 鼠标移动的绑定事件 只要移动就count++
handleMove=(e)=>{
e.target.innerHTML=count++
}
dom.onmousemove=handleMove
</script>
可以看到我鼠标移动的短暂几秒,计数器就从1加到了260,也就是回调函数在这几秒钟执行了260次,这种情况浏览器是吃的消,但是真实业务中事件的回调函数可能会很复杂,也可能是发送了ajax请求,这样频繁得被执行可能会造成页面卡顿。性能下降。这时就有两种主要的解决方法:防抖(debounce ),节流(throttle )
下面来通过防抖改进一下这个案例
防抖的思路:将回调函数放入一个定时器中经过指定的时间后执行,如果这段时间内又触发了事件,那么就将之前的定时器给取消,重新开始计时;所以说防抖的最终结果就是不论你触发多少次,我最终只执行最后一次
按照这个思路来写一下代码:
<script>
let count=1//计数变量
const dom =document.getElementById('content')
// 鼠标移动的绑定事件 只要移动就count++
handleMove=(e)=>{
e.target.innerHTML=count++
}
//防抖函数,传两个参数 第一个是事件绑定的回调函数,
//第二个是指定的延迟时间
debounce=(callback,waitTime)=>{
let timeout
return function(){
clearTimeout(timeout)
timeout=setTimeout(callback,waitTime)
}
}
//注意这里是将防抖函数的结果作为新的回调函数
dom.onmousemove=debounce(handleMove,500)
</script>
去浏览器看一下结果,发现防抖函数返回的回调并没有被执行,而且浏览器还报了个错:
也就是说,现在的事件对象e已经找不到了,可以想到,e是传给了新的回调函数,也就是12行返回的那个函数,但我并没有把他传给callback,那我是不是获取到参数传给callback就够了呢?
其实还是不行的,因为我们现在的函数还存在一个问题就是,callback的this指向已经发生变化,最初没有使用防抖的时候,handlMove指向的this是他绑定的dom节点,但是当我们放在定时器里时,this就会指向window
所以综上两点改进方向,1.保留this指向给callbck 2.把参数传给callback
那我们很容易想到使用bind 看一下新的debounce函数吧~
//防抖函数,传两个参数 第一个是事件绑定的回调函数,
//第二个是指定的延迟时间
debounce=function(callback,waitTime){
let timeout
return function(){
let args=arguments//arguments类数组的第一项就是事件对象
const self =this//把this保留下来
clearTimeout(timeout)
timeout=setTimeout(callback.bind(self,...args),waitTime)
}
}
看一下我们的成果~
nice~现在无论怎么移动鼠标,他也只会在我停下来的0.5s后触发回调了
好运不长(ಥ﹏ಥ),现在需求变了,要求页面我们中的这个回调函数,一触发就可以执行,但n秒内不会再执行,也就是把执行回调时间从结束放到了开始
防止需求一直变更!(好听点就叫可复用易维护)我们可以在防抖函数中,加上第三个参数immediate,如果是true 代表需要第一次触发就执行,如果是false第一次就不执行,而是放到最后执行,保持之前的思路
debounce=function(callback,waitTime,immediate){
let timeout
return function(){
let args=arguments//arguments类数组的第一项就是事件对象
const self =this//把this保留下来
if(timeout) clearTimeout(timeout)
if(immediate){
let callNow=!timeout//新设立一个变量与timeout相反
timeout=setTimeout(() => {
timeout=null //保证n秒之后还能再触发
}, waitTime);
if(callNow) callback(...arguments)
}else{
timeout=setTimeout(callback.bind(self,...args),waitTime)
}
}
}
我现在给debouce函数第三个参数传入true
dom.onmousemove=debounce(handleMove,1000,true)
看一下结果吧~
很好 实现成功~
新的业务需求又来了 万恶的甲方!(〃>皿<)
我们现在immediate设置为true,他第一次执行一定要等到n秒之后才能再次触发,甲方现在等不及了,说你给我一个按钮,我点击了之后,可以取消计时,马上就能触发执行
╮(╯▽╰)╭那就改代码咯~
debounce=function(callback,waitTime,immediate){
let timeout
//和刚刚的区别就是我给返回的函数取了个名字
//这样我方便给返回的函数添加一个cancle的方法属性,用来取消
const _debounce=function(){
let args=arguments//arguments类数组的第一项就是事件对象
const self =this//把this保留下来
if(timeout) clearTimeout(timeout)
if(immediate){
let callNow=!timeout//新设立一个变量与timeout相反
timeout=setTimeout(() => {
timeout=null //保证n秒之后还能再触发
}, waitTime);
if(callNow) callback(...arguments)
}else{
timeout=setTimeout(callback.bind(self,...args),waitTime)
}
}
//只要cancel函数一运行,我就取消计时,同时让timeout为Null
//这样才能保证点击了之后马上运行
_debounce.cancel=function(){
if(timeout) clearTimeout(timeout)
timeout=null
}
return _debounce
}
那我们只要给button绑定点击事件就好,一点击 就触发cancel函数
let handle=debounce(handleMove,10000,true)
dom.onmousemove=handle
//
btn.onclick=()=>{
handle.cancel()
}
现在看一下执行效果吧~
是不是非常的nice~
手写系列之防抖与防抖Plus就到这啦
感谢阅读,欢迎指正~
文章来自于本人公众号,觉得有帮助,感兴趣的话可以关注一下:西元前的小铁匠
参考:
JavaScript专题之跟着underscore学防抖
github Owner: mqyqingfeng