前言: 整個(gè)六月份就水了四篇文章,這完美的證明了我六月份基本沒(méi)學(xué)啥東西 ??。這周在工作中主要是寫(xiě)了消息系統(tǒng)模塊,自己封裝的,測(cè)試?yán)习l(fā)現(xiàn) bug ,只能發(fā)現(xiàn)一個(gè)修一個(gè),還好是小姐姐,不然都要過(guò)來(lái)揍我了,新項(xiàng)目可的仔細(xì)點(diǎn),多看 PRD 多測(cè)試。
主題:工作中我小伙伴喜歡用 lodash 這個(gè)庫(kù),我就喜歡 ES6 ,因?yàn)樵谖艺J(rèn)識(shí)中,ES6 處理數(shù)據(jù)已經(jīng)挺完美了,而且一個(gè)高手還能把處理數(shù)據(jù)的代碼全給壓平,代碼結(jié)構(gòu)由立體變扁平,類似這種:
//本人項(xiàng)目截取片段,寫(xiě)的不是很好多包涵
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));
其實(shí)我也是不想看 lodash,immediate這種第三方庫(kù),那么多 API,emmm,但是 ES6 也不是完美無(wú)缺的,其中就少了一個(gè)很重的東西,防抖和節(jié)流函數(shù)。而恰恰這兩個(gè)函數(shù)在項(xiàng)目中很常用到。這時(shí)候就能體驗(yàn)到 lodash 的便捷了,雖然它的代碼體積很大。最近我在寫(xiě)登陸模塊的按鈕的時(shí)候就體會(huì)很深,想起我以前技術(shù)差的時(shí)候能把項(xiàng)目模塊功能寫(xiě)出來(lái)就很 happy 了,現(xiàn)在開(kāi)始有點(diǎn)精力去注意這些邊邊角角了,that's brilliant。本著學(xué)習(xí)的態(tài)度,當(dāng)然的研究下 debounce and throttle 的源碼了,因?yàn)槿绻皇褂?lodash 第三方庫(kù)的時(shí)候,我們可以在項(xiàng)目中 util 文件夾中,放一些項(xiàng)目工具函數(shù)的地方,引入這兩個(gè)函數(shù),然后愉快的使用。
underscore 和 lodashjs 這兩個(gè)庫(kù)都有 debounce and throttle 用法都差不多,就以 underscore 為例進(jìn)行研究吧。
先推薦一個(gè)視頻教程看看思路:手寫(xiě)函數(shù)防抖和節(jié)流——小馬哥_老師
還有一個(gè)文字版的教程也可以看看:underscore 函數(shù)去抖的實(shí)現(xiàn)
在去官網(wǎng)看看 debounce 和 throttle 的用法:
我了解的防抖和節(jié)流:
- 防抖:多次觸發(fā),只執(zhí)行一次。
- 節(jié)流:一直觸發(fā)期間合理執(zhí)行。
這是個(gè) underscore CDN 可以打開(kāi)對(duì)照源碼閱讀:underscore
一、基本骨架

鼠標(biāo)移動(dòng)無(wú)限次觸發(fā)計(jì)數(shù)顯示:
<!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 實(shí)現(xiàn)
underscore 中 debounce 函數(shù)有三個(gè)參數(shù):debounce(需要防抖的函數(shù),間隔時(shí)間,執(zhí)行順序)
首先講下實(shí)現(xiàn)的三個(gè)難點(diǎn):
- 需要防抖函數(shù)中的 this;
- 需要防抖函數(shù)的事件對(duì)象 event ;
- 需要防抖函數(shù)返回結(jié)果不能改變
一版:能防抖、能綁定 this 和 event
<script>
let count = 0;
// debounce是一個(gè)高階函數(shù)
function debounce(func,wait){
let timeout,context;
return function(...args){
// 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
//args就是事件對(duì)象event
context = this;
// 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
if(timeout) clearTimeout(timeout);
// 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
timeout = setTimeout(function(){
timeout = null;
// func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
func.apply(context,args);
},wait);
}
}
function wirteCount(){
this.innerHTML = count++;
}
// debounce被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = debounce(wirteCount,1000);
</script>
二版:增加函數(shù)返回值
如果需要防抖的函數(shù) wirteCount 函數(shù)有返回值我們也應(yīng)該予以保留。
function wirteCount(){
this.innerHTML = count++;
return "我需要返回一些東西";
}
二版實(shí)現(xiàn),就增加了一個(gè) result 變量來(lái)接收 wirteCount 函數(shù)返回值:
<script>
let count = 0;
// debounce是一個(gè)高階函數(shù)
function debounce(func,wait){
let timeout,context,result;
return function(...args){
// 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
//args就是事件對(duì)象event
context = this;
// 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
if(timeout) clearTimeout(timeout);
// 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
timeout = setTimeout(function(){
timeout = null;
// func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
result = func.apply(context,args);
},wait);
return result;
}
}
function wirteCount(){
this.innerHTML = count++;
return "我需要返回一些東西";
}
// debounce被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = debounce(wirteCount,1000);
</script>
最最難的點(diǎn)來(lái)了,debounce 第三個(gè)參數(shù)的實(shí)現(xiàn),定義防抖函數(shù)剛觸發(fā)就執(zhí)行,還是觸發(fā)之后等 wait 秒在執(zhí)行。

三版:
<!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是一個(gè)高階函數(shù)
function debounce(func,wait,immediate){
let timeout,context,result;
return function(...args){
// 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
//args就是事件對(duì)象event
context = this;
// 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
if(timeout) clearTimeout(timeout);
if(immediate){
// 第一次觸發(fā),timeout===undefined恰好可以利用timeout的值
const callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
},wait);
if(callNow) result = func.apply(context,args);
}else{
// 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
timeout = setTimeout(function(){
timeout = null;
// func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
result = func.apply(context,args);
},wait);
}
return result;
}
}
function wirteCount(){
if(this.id === "box"){
this.innerHTML = count++;
}else{
this.innerHTML = idx++;
}
return "我需要返回一些東西";
}
// debounce被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = debounce(wirteCount,500,false);
box.onmousemove = debounce(wirteCount,500,true);
</script>
</body>
</html>
四版:現(xiàn)在就差一個(gè)取消操作了,取消操作我們需要做些改變,需要把 debounce 函數(shù)返回的函數(shù)提取出來(lái)進(jìn)行擴(kuò)展。

<!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是一個(gè)高階函數(shù)
function debounce(func, wait, immediate) {
let timeout, context, result;
function resDebounced(...args) {
// 這個(gè)函數(shù)里面的this就是要防抖函數(shù)要的this
//args就是事件對(duì)象event
context = this;
// 一直觸發(fā)一直清除上一個(gè)打開(kāi)的延時(shí)器
if (timeout) clearTimeout(timeout);
if (immediate) {
// 第一次觸發(fā),timeout===undefined恰好可以利用timeout的值
const callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
// 停止觸發(fā),只有最后一個(gè)延時(shí)器被保留
timeout = setTimeout(function() {
timeout = null;
// func綁定this和事件對(duì)象event,還差一個(gè)函數(shù)返回值
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被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = implement;
// 取消防抖
btn.onclick = implement.cancal;
</script>
</body>
</html>
debounce 到此就寫(xiě)完了,到此你能看懂幾乎所有第三方源碼實(shí)現(xiàn)了,因?yàn)樗鼈兊膶?shí)現(xiàn)基本都大同小異。
三、throttle 實(shí)現(xiàn)
你一定認(rèn)為 debounce 都實(shí)現(xiàn)了,throttle 就不是很難了,No no 。throttle 最難的是第三個(gè)參數(shù)的實(shí)現(xiàn)思路,先來(lái)看看 underscore 中 throttle 的用法,throttle 前兩個(gè)參數(shù)和 debounce 沒(méi)啥區(qū)別,區(qū)別在于第三個(gè)參數(shù)不是 boolean 值,而是一個(gè)對(duì)象_throttle(func,wait,{leading: true,trailing:true}) leading 表示事件觸發(fā)立即執(zhí)行 func ,trailing 表示最后離開(kāi)是否觸發(fā) func。兩個(gè)都默認(rèn)為 true。
前置知識(shí):
和 debounce 函數(shù)一樣,也有三個(gè)難點(diǎn):
- 需要防抖函數(shù)中的 this,通過(guò) apply 綁定;
- 需要防抖函數(shù)的事件對(duì)象 event ,通過(guò) apply 傳入;
- 需要防抖函數(shù)返回結(jié)果不能改變
現(xiàn)在這個(gè)就比較簡(jiǎn)單了,通過(guò)這三個(gè)變量 let ctx, args, result; 完美接受實(shí)現(xiàn),下面主要關(guān)注實(shí)現(xiàn) throttle 第三個(gè)參數(shù)的實(shí)現(xiàn)。
3.1 leading 實(shí)現(xiàn)
leading :函數(shù)一觸發(fā)就立即執(zhí)行 func ,然后穩(wěn)定的間隔執(zhí)行 func ,最后一次離開(kāi)不執(zhí)行 func。
下面通過(guò)時(shí)間戳來(lái)實(shí)現(xiàn)的:
- 剛開(kāi)始
old = 0條件now - old > wait一定為真,也就是 func 立即觸發(fā)。 -
now - old > wait第一次為真之后,func 就能穩(wěn)定執(zhí)行。 - 最后離開(kāi)不會(huì)執(zhí)行 func ,快速進(jìn)入快速離開(kāi),你會(huì)發(fā)現(xiàn) func 只在進(jìn)入執(zhí)行了一次。
<!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是一個(gè)高階函數(shù)
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被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = implement;
</script>
</body>
</html>
3.2 trailing 的實(shí)現(xiàn)
- 第一次進(jìn)入不觸發(fā),然后穩(wěn)定的間隔執(zhí)行 func ,最后一次離開(kāi)執(zhí)行 func。
- 快速進(jìn)入快速離開(kāi),你會(huì)發(fā)現(xiàn) func 只在離開(kāi)后執(zhí)行了一次。
<!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被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = implement;
</script>
</body>
</html>
3.3 leading 和 trailing 二合一實(shí)現(xiàn) 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);
// 只要執(zhí)行func,old時(shí)間戳就的重置
old = Date.now();
timeout = null;
}
function resThrottle(){
ctx = this;
args = arguments;
let now = Date.now();
// 第一次觸發(fā)函數(shù)是否執(zhí)行
if(options.leading === false && !old){
old = now;
}
if(now - old > wait){
// 當(dāng)條件now - old > wait為假時(shí),會(huì)開(kāi)啟延時(shí)器
// 所以我們要清除下
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被執(zhí)行必須返回一個(gè)函數(shù)
app.onmousemove = implement;
//
btn.onclick = implement.cancal;
</script>
</body>
</html>
寫(xiě)作于北京昌平區(qū) 當(dāng)前時(shí)間 Saturday, June 27, 2020 02:29:33