debounce and throttle
前言: 整个六月份就水了四篇文章,这完美的证明了我六月份基本没学啥东西 😤。这周在工作中主要是写了消息系统模块,自己封装的,测试老发现 bug ,只能发现一个修一个,还好是小姐姐,不然都要过来揍我了,新项目可的仔细点,多看 PRD 多测试。
主题:工作中我小伙伴喜欢用 lodash 这个库,我就喜欢 ES6 ,因为在我认识中,ES6 处理数据已经挺完美了,而且一个高手还能把处理数据的代码全给压平,代码结构由立体变扁平,类似这种:
//本人项目截取片段,写的不是很好多包涵
let unReadHeadIdArr = res.data.messages.map(item=>item && item.head.id);
let allMessArrId = window.sessionStorage.getItem("allMessVisable");
allMessArrId = allMessArrId ? JSON.parse(allMessArrId) : [];
let unReadHeadId = unReadHeadIdArr.filter(item=>!allMessArrId.includes(item));
其实我也是不想看 lodash,immediate这种第三方库,那么多 API,emmm,但是 ES6 也不是完美无缺的,其中就少了一个很重的东西,防抖和节流函数。而恰恰这两个函数在项目中很常用到。这时候就能体验到 lodash 的便捷了,虽然它的代码体积很大。最近我在写登陆模块的按钮的时候就体会很深,想起我以前技术差的时候能把项目模块功能写出来就很 happy 了,现在开始有点精力去注意这些边边角角了,that's brilliant。本着学习的态度,当然的研究下 debounce and throttle
的源码了,因为如果不使用 lodash 第三方库的时候,我们可以在项目中 util 文件夹中,放一些项目工具函数的地方,引入这两个函数,然后愉快的使用。
underscore
和 lodashjs
这两个库都有 debounce and throttle
用法都差不多,就以 underscore
为例进行研究吧。
先推荐一个视频教程看看思路:手写函数防抖和节流——小马哥_老师
还有一个文字版的教程也可以看看:underscore 函数去抖的实现
在去官网看看 debounce 和 throttle 的用法:
我了解的防抖和节流:
- 防抖:多次触发,只执行一次。
- 节流:一直触发期间合理执行。
这是个 underscore CDN 可以打开对照源码阅读:underscore
一、基本骨架
鼠标移动无限次触发鼠标移动无限次触发计数显示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
div{
height: 300px;
width: 900px;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 38px;
background-color: #222;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
let count = 0;
app.onmousemove = function(){
this.innerHTML = count++;
}
</script>
</body>
</html>
二、debounce 实现
underscore 中 debounce 函数有三个参数:debounce(需要防抖的函数,间隔时间,执行顺序)
首先讲下实现的三个难点:
- 需要防抖函数中的 this;
- 需要防抖函数的事件对象 event ;
- 需要防抖函数返回结果不能改变
一版:能防抖、能绑定 this 和 event
<script>
let count = 0;
// debounce是一个高阶函数
function debounce(func,wait){
let timeout,context;
return function(...args){
// 这个函数里面的this就是要防抖函数要的this
//args就是事件对象event
context = this;
// 一直触发一直清除上一个打开的延时器
if(timeout) clearTimeout(timeout);
// 停止触发,只有最后一个延时器被保留
timeout = setTimeout(function(){
timeout = null;
// func绑定this和事件对象event,还差一个函数返回值
func.apply(context,args);
},wait);
}
}
function wirteCount(){
this.innerHTML = count++;
}
// debounce被执行必须返回一个函数
app.onmousemove = debounce(wirteCount,1000);
</script>
二版:增加函数返回值
如果需要防抖的函数 wirteCount
函数有返回值我们也应该予以保留。
function wirteCount(){
this.innerHTML = count++;
return "我需要返回一些东西";
}
二版实现,就增加了一个 result 变量来接收 wirteCount
函数返回值:
<script>
let count = 0;
// debounce是一个高阶函数
function debounce(func,wait){
let timeout,context,result;
return function(...args){
// 这个函数里面的this就是要防抖函数要的this
//args就是事件对象event
context = this;
// 一直触发一直清除上一个打开的延时器
if(timeout) clearTimeout(timeout);
// 停止触发,只有最后一个延时器被保留
timeout = setTimeout(function(){
timeout = null;
// func绑定this和事件对象event,还差一个函数返回值
result = func.apply(context,args);
},wait);
return result;
}
}
function wirteCount(){
this.innerHTML = count++;
return "我需要返回一些东西";
}
// debounce被执行必须返回一个函数
app.onmousemove = debounce(wirteCount,1000);
</script>
最最难的点来了,debounce 第三个参数的实现,定义防抖函数刚触发就执行,还是触发之后等 wait 秒在执行。
三版:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
section{
margin: 50px auto;
width: 900px;
height: 300px;
}
div{
height: 300px;
width: 400px;
float: left;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
margin-left: 50px;
font-size: 38px;
background-color: #222;
}
</style>
</head>
<body>
<section>
<div id="app"></div>
<div id="box"></div>
</section>
<script>
let count = 0;
let idx = 0;
// debounce是一个高阶函数
function debounce(func,wait,immediate){
let timeout,context,result;
return function(...args){
// 这个函数里面的this就是要防抖函数要的this
//args就是事件对象event
context = this;
// 一直触发一直清除上一个打开的延时器
if(timeout) clearTimeout(timeout);
if(immediate){
// 第一次触发,timeout===undefined恰好可以利用timeout的值
const callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
},wait);
if(callNow) result = func.apply(context,args);
}else{
// 停止触发,只有最后一个延时器被保留
timeout = setTimeout(function(){
timeout = null;
// func绑定this和事件对象event,还差一个函数返回值
result = func.apply(context,args);
},wait);
}
return result;
}
}
function wirteCount(){
if(this.id === "box"){
this.innerHTML = count++;
}else{
this.innerHTML = idx++;
}
return "我需要返回一些东西";
}
// debounce被执行必须返回一个函数
app.onmousemove = debounce(wirteCount,500,false);
box.onmousemove = debounce(wirteCount,500,true);
</script>
</body>
</html>
四版:现在就差一个取消操作了,取消操作我们需要做些改变,需要把 debounce 函数返回的函数提取出来进行扩展。
2S内可以取消事件执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
div {
height: 300px;
width: 400px;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 38px;
background-color: #222;
}
button{
display: block;
height: 30px;
width: 60px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<button id="btn">取消</button>
<script>
let count = 0;
let idx = 0;
// debounce是一个高阶函数
function debounce(func, wait, immediate) {
let timeout, context, result;
function resDebounced(...args) {
// 这个函数里面的this就是要防抖函数要的this
//args就是事件对象event
context = this;
// 一直触发一直清除上一个打开的延时器
if (timeout) clearTimeout(timeout);
if (immediate) {
// 第一次触发,timeout===undefined恰好可以利用timeout的值
const callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
// 停止触发,只有最后一个延时器被保留
timeout = setTimeout(function() {
timeout = null;
// func绑定this和事件对象event,还差一个函数返回值
result = func.apply(context, args);
}, wait);
}
return result;
}
resDebounced.cancal = function(){
clearTimeout(timeout);
timeout = null;
}
return resDebounced;
}
function wirteCount() {
this.innerHTML = count++;
return "我需要返回一些东西";
}
const implement = debounce(wirteCount, 2000, false);
// debounce被执行必须返回一个函数
app.onmousemove = implement;
// 取消防抖
btn.onclick = implement.cancal;
</script>
</body>
</html>
debounce 到此就写完了,到此你能看懂几乎所有第三方源码实现了,因为它们的实现基本都大同小异。
三、throttle 实现
你一定认为 debounce 都实现了,throttle 就不是很难了,No no 。throttle 最难的是第三个参数的实现思路,先来看看 underscore 中 throttle 的用法,throttle 前两个参数和 debounce 没啥区别,区别在于第三个参数不是 boolean 值,而是一个对象_throttle(func,wait,{leading: true,trailing:true})
leading 表示事件触发立即执行 func ,trailing 表示最后离开是否触发 func。两个都默认为 true。
前置知识:
和 debounce
函数一样,也有三个难点:
- 需要防抖函数中的 this,通过 apply 绑定;
- 需要防抖函数的事件对象 event ,通过 apply 传入;
- 需要防抖函数返回结果不能改变
现在这个就比较简单了,通过这三个变量 let ctx, args, result;
完美接受实现,下面主要关注实现 throttle
第三个参数的实现。
3.1 leading 实现
leading :函数一触发就立即执行 func ,然后稳定的间隔执行 func ,最后一次离开不执行 func。
下面通过时间戳来实现的:
- 刚开始
old = 0
条件now - old > wait
一定为真,也就是 func 立即触发。 -
now - old > wait
第一次为真之后,func 就能稳定执行。 - 最后离开不会执行 func ,快速进入快速离开,你会发现 func 只在进入执行了一次。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
div {
height: 300px;
width: 400px;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 38px;
background-color: #222;
}
button{
display: block;
height: 30px;
width: 60px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<button id="btn">取消</button>
<script>
let count = 0;
let idx = 0;
// throttle是一个高阶函数
function throttle(func,wait){
let ctx,args,result;
let old = 0;
return function(){
ctx = this;
args = arguments;
let now = Date.now();
if(now - old > wait){
result = func.apply(ctx,args);
old = now;
};
return result;
}
}
function wirteCount() {
this.innerHTML = count++;
return "我需要返回一些东西";
}
const implement = throttle(wirteCount, 1000);
// throttle被执行必须返回一个函数
app.onmousemove = implement;
</script>
</body>
</html>
3.2 trailing 的实现
- 第一次进入不触发,然后稳定的间隔执行 func ,最后一次离开执行 func。
- 快速进入快速离开,你会发现 func 只在离开后执行了一次。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
div {
height: 300px;
width: 400px;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 38px;
background-color: #222;
}
button{
display: block;
height: 30px;
width: 60px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<button id="btn">取消</button>
<script>
let count = 0;
let idx = 0;
function throttle(func,wait){
let ctx,args,result,timeout;
return function(){
ctx = this;
args = arguments;
if(!timeout){
timeout = setTimeout(function(){
timeout = null;
result = func.apply(ctx,args);
},wait);
};
return result;
}
}
function wirteCount() {
this.innerHTML = count++;
return "我需要返回一些东西";
}
const implement = throttle(wirteCount, 1000);
// throttle被执行必须返回一个函数
app.onmousemove = implement;
</script>
</body>
</html>
3.3 leading 和 trailing 二合一实现 throttle
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>debounce and throttle</title>
<style>
div {
height: 300px;
width: 400px;
margin: 50px auto;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 38px;
background-color: #222;
}
button{
display: block;
height: 30px;
width: 60px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="app"></div>
<button id="btn">取消</button>
<script>
let count = 0;
let idx = 0;
function throttle(func,wait,options = {}){
let ctx, args, result, timeout, old = 0;
let later = function(){
result = func.apply(ctx,args);
// 只要执行func,old时间戳就的重置
old = Date.now();
timeout = null;
}
function resThrottle(){
ctx = this;
args = arguments;
let now = Date.now();
// 第一次触发函数是否执行
if(options.leading === false && !old){
old = now;
}
if(now - old > wait){
// 当条件now - old > wait为假时,会开启延时器
// 所以我们要清除下
if(timeout){
clearTimeout(timeout);
timeout = null;
}
result = func.apply(ctx,args);
old = now;
}else if(!timeout && options.trailing !== false){
timeout = setTimeout(later,wait);
};
return result;
}
resThrottle.cancal = function(){
clearTimeout(timeout);
old = 0;
timeout = context = args = null;
};
return resThrottle;
}
function wirteCount() {
this.innerHTML = count++;
return "我需要返回一些东西";
}
const implement = throttle(wirteCount, 5000);
// throttle被执行必须返回一个函数
app.onmousemove = implement;
//
btn.onclick = implement.cancal;
</script>
</body>
</html>
写作于北京昌平区 当前时间 Saturday, June 27, 2020 02:29:33