本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1.学习内容
- 可以克隆源码也可以直接引用cdn写demo
- 参考文章 JavaScript专题之跟着underscore学防抖
- underscore 源码
- github.com/jashkenas/u…
- 可以看测试用例学习 underscore debounce 测试用例
- lodash 源码
- lodash.com/docs/4.17.1…
- github.com/lodash/loda…
- 参考文章:深入浅出防抖函数 debounce
2.“防抖”关键词出现的背景
在各种各样的浏览器事件中,需要特别关注的事件是那些容易过度触发的事件。
| 事件类型 | 事件方法名 |
|---|---|
| window上的事件 | scroll,resize 等 |
| 鼠标事件 | mousedown、mousemove、mouseover 等 |
| 键盘事件 | keyup、keydown |
以上列出的事件都是存在被频繁触发的风险,频繁触发可能会引发页面的抖动甚至卡顿。为了规避这种情况,需要一些手段来控制事件被触发的频率。就是在这样的背景下,throttle(事件节流)和 debounce(事件防抖)出现了。
3."防抖"的定义
防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。
防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。
生动化的解释:
(1)假定在做公交车时,司机需等待最后一个人进入后再关门。
(2)每次新进一个人,司机就会把计时器清零并重新开始计时,重新等待 1 分钟再关门。
(3)如果后续 1 分钟内都没有乘客上车,司机会认为乘客都上来了,司机会认为确实没有人需要搭这趟车了,将关门发车。
上述例子和防抖函数的对应关系:
(1)「上车的乘客」就是我们频繁操作事件而不断涌入的回调任务;
(2)「1 分钟」就是计时器,它是司机决定「关门」的依据,如果有新的「乘客」上车,将清零并重新计时;
(3)「关门」就是最后需要执行的函数。
4.防抖函数的实现
4.1 准备工作
要模拟一个块级元素的滚动操作,定义一个容器container限制高度为300px,内部定义4个小块加起来总高度超出container即可。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style>
.container {
border: 1px solid #4DC7EC;
width: 200px;
height: 300px;
overflow-y: auto;
text-align: center;
margin: 0 auto;
}
.block {
border: 1px solid #4BA946;
margin-left: 50px;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div class="container">
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
<div class="block"></div>
</div>
</body>
<script src="./src/index.js"></script>
</html>
效果如下:
为container监听scroll事件(index.js代码):
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this',this)
console.log('滚动了')
}
container.addEventListener('scroll', handleScroll)
轻轻触发一下滚动就调用了好多次handleScroll:
4.2 版本1:简单地增加延时
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this',this)
console.log('滚动了')
}
function debounce(func,wait) {
let timeout
return function() {
clearTimeout(timeout)
timeout = setTimeout(func,wait)
}
}
container.addEventListener('scroll', debounce(handleScroll,1000))
debounce函数最后得返回一个函数,函数中利用setTimeout延迟执行了被包装的函数。看一下执行的效果:
轻轻滚动一下,控制台只输出了一次。观察输出的内容,发现如下问题:
(1)事件对象获取不到了
(2)this指针由指向container变为指向了window
继续优化,来解决这些问题
4.3 版本2:解决this指针的指向问题
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this',this)
console.log('滚动了')
}
function debounce(func,wait) {
let timeout
return function() {
const context = this;
clearTimeout(timeout)
timeout = setTimeout(func.apply(context),wait)
}
}
container.addEventListener('scroll', debounce(handleScroll,1000))
this指针之所以改变了是因为把setTimeout包裹了,由于setTimeout是在全局作用下实现的,是window上的方法,所以this指针指向了window。这个版本先保存了调用者上下文context,然后使用apply方法指定func函数的this。
我们看一下调用结果:
4.4 版本3 解决event对象为undefined问题
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this',this)
console.log('滚动了')
}
function debounce(func,wait) {
let timeout
return function() {
const args = arguments
const context = this;
clearTimeout(timeout)
timeout = setTimeout(func.apply(context,args),wait)
}
}
container.addEventListener('scroll', debounce(handleScroll,1000))
版本1中被debounce包裹后的handleScroll之所以不能输出事件对象是因为debounce函数返回的函数中在调用func时,没有给其传递参数。而如上代码获取了包裹函数的arguments参数(一个类数组对象),然后调用func(handleScroll)的时候传递进去。
下面的代码在运行效果上和上面的代码等价:
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this',this)
console.log('滚动了')
}
function debounce(func,wait) {
let timeout
return function(e) {
const context = this;
clearTimeout(timeout)
timeout = setTimeout(func.apply(context,[e]),wait)
}
}
container.addEventListener('scroll', debounce(handleScroll,1000))
可能比较好理解,但是没有直接使用arguments简洁。
代码的运行效果如下:
4.5 版本4 立刻执行
如果希望立刻执行函数,然后等待一段时间才可以触发重新执行则需要继续对代码修改:
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this', this)
console.log('滚动了')
}
function debounce(func, wait, immediate) {
let timeout
return function() {
const args = arguments
const context = this;
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args)
}, wait)
if (callNow) {
func.apply(context, arguments)
}
} else {
timeout = setTimeout(func.apply(context, arguments), wait)
}
}
}
container.addEventListener('scroll', debounce(handleScroll, 1000,true))
整体的逻辑是判断是否立刻执行,两个分支。如果不是立刻执行则和原来的代码一样。立刻执行的时候要判断是否可以立刻调用,只有当前timeout为假的情况下也就是没有延时的时候才可以立刻调用。否则不立刻调用,开始定时调用。
4.6 版本5 立刻调用的时候要返回值
如果被debounce包装的函数可以有返回值,想把返回值也获取到
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this', this)
console.log('滚动了')
return 1
}
function debounce(func, wait, immediate) {
let timeout,result
return function() {
const args = arguments
const context = this;
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args)
}, wait)
if (callNow) {
result = func.apply(context, arguments)
}
} else {
timeout = setTimeout(func.apply(context, arguments), wait)
}
console.log('result', result)
return result
}
}
container.addEventListener('scroll', debounce(handleScroll, 1000,true))
例如上面的代码handleScroll当中但会了一个1,则立刻执行的时候会得到这个返回值,然后返回。下图展示了运行效果:
立刻执行的时候有返回值,然后进入到异步的触发则没有返回值,打印不出来1。
另外感觉事件监听这种形式的用法没办法利用返回值,例如:
container.addEventListener('scroll', debounce(handleScroll, 1000,true))
这里handleScroll的返回值怎么利用呢?没太想明白~像一些复杂计算的情况可以:
const res = debounce(computeSomething, 1000,true)
4.7 版本6:取消防抖
希望能取消 debounce 函数,若immediate 为 true,则只有等 10 秒后才能重新触发事件,希望有一个按钮,点击后,取消防抖,这样再去触发,就可以立刻执行。
var container = document.getElementsByClassName('container')[0];
function handleScroll(e) {
console.log('事件对象', e)
console.log('this', this)
console.log('滚动了')
return 1
}
function debounce(func, wait, immediate) {
let timeout,result
const debounced = function() {
const args = arguments
const context = this;
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args)
}, wait)
if (callNow) {
result = func.apply(context, arguments)
}
} else {
timeout = setTimeout(func.apply(context, arguments), wait)
}
console.log('result', result)
return result
}
debounced.cancel = function(){
clearTimeout(timeout)
timeout = null;
}
return debounced
}
const betterScroll = debounce(handleScroll, 2000,true)
container.addEventListener('scroll', betterScroll)
function cancelDebounce() {
betterScroll.cancel()
}
debounce函数中不再是一个匿名函数了,而是名为debounced的函数,并在debounced增加一个cancel属性,为一个函数,清除定时器。
在html代码中增加一个按钮,用于取消防抖:
<div style="margin:0 auto; width:200px;text-align: center;">
<button onclick="cancelDebounce()">取消防抖</button>
</div>
4.8 underscore的debounce
import restArguments from './restArguments.js';
import now from './now.js';
// When a sequence of calls of the returned function ends, the argument
// function is triggered. The end of a sequence is defined by the `wait`
// parameter. If `immediate` is passed, the argument function will be
// triggered at the beginning of the sequence instead of at the end.
export default function debounce(func, wait, immediate) {
var timeout, previous, args, result, context;
var later = function() {
var passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
if (!immediate) result = func.apply(context, args);
// This check is needed because `func` can recursively invoke `debounced`.
if (!timeout) args = context = null;
}
};
var debounced = restArguments(function(_args) {
context = this;
args = _args;
previous = now();
if (!timeout) {
timeout = setTimeout(later, wait);
if (immediate) result = func.apply(context, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = args = context = null;
};
return debounced;
}
看完感觉这个高大上,但是可读性不是很强。抽象出later函数,还是一个递归的函数。在debounced函数中调用了later。理解的时候要从调用处开始,debounced调用later,所以先从debounced看。
在debounced给previous赋值为当前时间,表示本次调用的时间。然后判断timeout,不存在timeout则等待wait后执行later。然后判断是否是立刻调用,如果是则调用。
在later函数中,计算经过了多长时间passed 。如果wait大于passed说明延时还没有结束,继续延时,但是时间更新为wait-passed 也就是剩余的时间。否则延时时间已经到了可以执行函数了。
5.收货总结
1.了解闭包的应用场景之一(实现防抖)
2.了解防抖的背景,含义,应用场景和实现方法
3.了解underscore函数库中防抖函数的实现思路