- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第25期,链接:juejin.cn/post/708744…
什么是防抖、节流
防抖(debounce): 在持续触发事件时,不会立即执行,会等待n秒再执行事件。在n秒内重复触发事件,只执行最后一次。如果在n秒到来之前,又触发了事件,则重新开启定时器,执行最后一次触发事件。
节流(throttle): 在持续触发事件时,在n秒只执行一次函数。如果在秒内,再次触发此事件,则直接忽略不执行。其主要目的就是减少一段时间的触发频率。
页面准备:
// index.html
<!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>
<div id="container"></div>
<script src="debounce.js"></script>
</body>
</html>
// debounce.js
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = getUserAction
限定时间内不可重复触发事件
function debounce(fn, wait){
let timer;
return function(){
clearTimeout(timer)
timer = setTimeout(fn, wait)
}
}
// 1s 内不触发
container.onmousemove = debounce(getUserAction, 1000)
改变this
上面的 this 会默认指向 window, 我们需要将其改到触发函数的本身上 div #container。需要用 apply 改变指向
function debounce(fn, wait){
let timer;
return function(){
var context = this
clearTimeout(timer)
timer = setTimeout(fn.apply(context), wait)
}
}
argument
不使用 debounce 函数,getUserAction 传入 e,会打印出 mounseEvent 对象,但是使用了 debounce,e 的结果为 undefined
function debounce(fn, wait){
let timer;
return function(){
var context = this;
var args = argument
clearTimeout(timer);
timeout = setTimeout(fn.apply(context, args), wait)
}
}
getUserAction 返回值
getUserAction 函数可能有返回值, 所以我们也要返回函数的执行结果。
function debounce(fn, wait){
let timer;
let result;
return function(){
var context = this;
var args = argument
clearTimeout(timer);
timeout = setTimeout(()=>{
result = fn.apply(context, args)
}, wait)
return result;
}
}
immediate 立即执行
function debounce(fn, wait, immediate){
let timer;
let result;
return function(){
var context = this;
var args = argument
if(timer) clearTimeout(timer);
// immediate 为 true,表示第一次触发后执行
// timer 为 null, 表示首次触发
// 在 wait = 5s 内, 不再执行
// 在 5s 到了时,timer 设为 null,此时 callNow 为 true,又可以执行事件了
if(immediate){
let callNow = !timer;
timer = setTimeout(function(){
result = null;
},wait)
if(callNow) result = fn.apply(context, args)
} else{
timer = setTimeout(()=>{
result = fn.apply(context, args)
}, wait)
}
return result;
}
}
取消防抖
function debounce(fn, wait, immediate){
let timer;
let result;
var debounced = function(){
var context = this;
var args = argument
if(timer) clearTimeout(timer);
// immediate 为 true,表示第一次触发后执行
// timer 为空 表示首次触发
if(immediate){
let callNow = !timer;
timer = setTimeout(function(){
result = null;
},wait)
if(callNow) {
result = fn.apply(context, args)
}
} else{
timer = setTimeout(()=>{
result = fn.apply(context, args)
}, wait)
}
return result;
}
debounced.cancel = function(){
clearTimeout(timer)
timer = null
}
}
cancel 防抖运用:
const setUseAction = debounce(getUserAction, 1000, true)
container.onmousemove = setUseAction
document.getElementById('button').addEventListener('click', function() {
setUseAction.cancel()
})
underscore的防抖
// 获取当前时间戳
function now(){
return Date.now()|| new Date().getTime()
}
/**
*
* @param {*} func 回调函数
* @param {*} wait 等待时间
* @param {*} immediate 是否立即执行回调函数
* @returns
*/
function debounce(func, wait, immediate) {
var timeout, previous, args, result, context
var later = function () {
//计算是否超时
// passed: 距离上次执行的时间
// wait: 定时器时间
var passed = now() - previous
if (wait > passed) {
// 例子: wait 定时器为10s,passed: 距离上次执行7sl
// 所以 在 10 -7 = 3s后 执行 later 方法
//距离上一次调用已经过了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) {
console.log('_args1', _args);
// 将this指向的window 改为指向 当前 div
context = this
// _args: getUserAction 传入的 e, 会打印出 mouseEvent 对象
args = _args
previous = now();
if (!timeout) {
//第一次的时候,timeout为null。如果immediate = true,立即执行回调函数
timeout = setTimeout(later, wait)
if (immediate) result = func.apply(context, args)
}
return result
})
//取消防抖的定时器
debounced.cancel = function () {
clearTimeout(timeout)
timeout = args = context = null
}
return debounced
}
function restArguments(func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex
return function () {
var length = Math.max(arguments.length - startIndex, 0),
rest = Array(length),
index = 0
for (; index < length; index++) {
rest[index] = arguments[index + startIndex]
}
switch (startIndex) {
case 0: return func.call(this, rest)
case 1: return func.call(this, arguments[0], rest)
case 2: return func.call(this, arguments[0], arguments[1], rest)
}
var args = Array(startIndex + 1)
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index]
}
args[startIndex] = rest
return func.apply(this, args)
}
}
总结:
- 调试 underscore 源码,了解了防抖的原理
- 防抖的使用场景: 适用于输入框远程查询事件,在线文档自动保存,浏览器视口大小改变
- 节流的使用场景: 适用于按钮提交事件,页面滚动事件的触发,搜索框联想功能