看了《JavaScript高級程序設(shè)計》和網(wǎng)上的一些博客,感覺對函數(shù)節(jié)流和函數(shù)防抖的概念是反的鼓鲁,以下我寫的關(guān)于防抖和節(jié)流的概念取決于多數(shù)人的概念吧履肃,并且基于倫敦前端工程師David Corbacho的客座文章。文章寫的很好坐桩,并且有對應(yīng)的代碼可以操作尺棋,更容易理解。其實我覺得叫什么不重要绵跷,這個方法叫節(jié)流還是這個方法叫防抖膘螟,只要你能說明白,并且在生產(chǎn)中能用上就可以碾局,一個名字荆残,不用太去糾結(jié)。
《復(fù)仇者聯(lián)盟4:終局之戰(zhàn)》代表著一個時代的結(jié)束净当,從2008年高二看300多MB的《鋼鐵俠》開始内斯,漫威電影宇宙也像哈利波特的魔法世界一樣一路伴我前行。一個時代的落幕像啼,必將開始一個新的時代俘闯。End Game?忽冻?No真朗!
I LOVE YOU THREE THOUSANDS TIMES
I AM IRON MAN
banner獻(xiàn)給復(fù)仇者聯(lián)盟的超級英雄們??????
為什么要防抖和節(jié)流?僧诚?
防抖和節(jié)流是兩個相似的技術(shù)遮婶,都是為了減少一個函數(shù)無用的觸發(fā)次數(shù),以便提高性能或者說避免資源浪費(fèi)湖笨。我們都知道js在操作DOM的時候旗扑,代價非常昂貴,相對于非DOM操作需要更多的內(nèi)存和和CPU時間慈省,假如我們一個函數(shù)是在滾動滾動條或者更改更改窗口大小的時候頻繁觸發(fā)臀防,還是會出現(xiàn)頁面卡頓,如果是一套復(fù)雜的操作DOM邏輯辫呻,可能還會引起瀏覽器崩潰清钥。所以我們需要控制一下觸發(fā)的次數(shù),來優(yōu)化一下代碼執(zhí)行情況放闺。
口說無憑,大家可能也不了解到底是怎樣操作缕坎,那就來個例子:??
<!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>我要節(jié)流</title>
<style>
body{ height: 3000px; }
#centerNum { width: 100px; height: 100px; line-height: 100px; text-align: center; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body>
<h1 id="centerNum">0</h1>
<script>
var num = 0;
window.onscroll = function () {
var root = document.getElementsByTagName('body'),
h = document.getElementById('centerNum');
h.innerHTML = num;
num ++;
}
</script>
</body>
</html>
我們來一個window.onscroll
的函數(shù)怖侦,只要滾動,就改變一次<h1>
標(biāo)簽中的數(shù),在上面的圖中匾寝,我們能看到這個觸發(fā)是非常頻繁的搬葬,如果我們不加以干涉的話,讓這個函數(shù)肆意觸發(fā)艳悔,豈不是要上天了??
Debounce 防抖
什么是防抖
啥是防抖呢急凰?我自己的理解就是,當(dāng)連續(xù)觸發(fā)一個方法的時候猜年,方法并不執(zhí)行抡锈,而是在連續(xù)觸發(fā)結(jié)束的時候再執(zhí)行這個方法。
舉個例子:一部直梯乔外,陸續(xù)往上上人(連續(xù)觸發(fā))床三,當(dāng)不再上人的時候(停止連續(xù)觸發(fā)),電梯才會關(guān)門并動起來(執(zhí)行方法)杨幼。
如何實現(xiàn)呢
上面是我模擬電梯上人的例子做出來的撇簿,可能這樣看的比較直觀一些,下面有我實現(xiàn)的代碼差购,大概意思就是當(dāng)我上人以后四瘫,電梯啟動,當(dāng)我一直在上人的時候欲逃,電梯不動直到不再上人了离福,才會關(guān)門啟動
<!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>
</style>
</head>
<body>
<button id="addBtn">電梯上人附鸽,人數(shù)+1</button><button id="resetBtn">重置</button>
<p id="personNum">電梯人數(shù):0(假設(shè)電梯可以無限裝人)</p>
<p id="elevatorStatus">電梯停靠</p>
<script>
var personNum = 0; // 電梯人數(shù)
var closeDoor = null; // 電梯啟動延時程序
var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
var elevatorStatus = document.getElementById('elevatorStatus'); // 獲取電梯狀態(tài)標(biāo)簽
/**
* @method 電梯內(nèi)添加人數(shù)
* @description 點擊一次電梯內(nèi)增加一人,增加完人數(shù)電梯啟動初始化
*/
function addPerson() {
personNum ++;
personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
initElevatorStart();
}
/**
* @method 電梯啟動
* @description 電梯啟動挖函,置灰添加人數(shù)按鈕,禁止上人
*/
function elevatorStart() {
elevatorStatus.innerHTML = '電梯啟動';
addBtn.disabled = true;
}
/**
* @method 電梯啟動初始化
* @description 清除之前的關(guān)門延時袍祖,并重新計算關(guān)門延時500ms怖辆,意思是當(dāng)不在觸發(fā)電梯啟動初始化函數(shù)時,500ms后啟動電梯
*/
function initElevatorStart() {
clearTimeout(closeDoor);
closeDoor = setTimeout(function () {
elevatorStart();
}, 500);
}
/**
* @method 重置電梯
*/
function reset() {
personNum = 0;
personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
elevatorStatus.innerHTML = '電梯驮2ぃ靠';
addBtn.disabled = false;
}
addBtn.addEventListener('click', addPerson);
resetBtn.addEventListener('click', reset);
</script>
</body>
</html>
上面的代碼意思就是我電梯上一個人咬清,就需要關(guān)閉電梯門(觸發(fā)initElevatorStart()
方法),然后電梯啟動奴潘。但是我一直在點擊上人的按鈕旧烧,電梯是不會觸發(fā)關(guān)門啟動電梯的elevatorStart()
方法。
代碼的核心是initElevatorStart()
方法画髓,這個方法在實際需要執(zhí)行的關(guān)門啟動電梯方法elevatorStart()
外面添加了一層setTimeout
方法掘剪,也就是為了在調(diào)用這個方法的時候我們過500毫秒再去執(zhí)行真正需要執(zhí)行的方法。如果這500毫秒之內(nèi)奈虾,又重新觸發(fā)了initElevatorStart()
方法夺谁,就需要重新計時廉赔,要不不就夾到人了嘛,要賠錢的匾鸥。蜡塌。。勿负。
這是防抖最粗糙的實現(xiàn)了??????
基本形式
下面是這個防抖實現(xiàn)的最基本的形式馏艾,也是我們在《JavaScript高級程序設(shè)計》中看到的樣子??
var processor = {
timeoutId: null, // 相當(dāng)于延時setTimeout的一個標(biāo)記,方便清除的時候使用
// 實際進(jìn)行處理的方法
// 連續(xù)觸發(fā)停止以后需要觸發(fā)的代碼
performProcessiong: function () {
// 實際執(zhí)行的代碼
// 這里實際就是需要在停止觸發(fā)的時候執(zhí)行的代碼
},
// 初始處理調(diào)用的方法
// 在實際需要觸發(fā)的代碼外面包一層延時clearTimeout方法奴愉,以便控制連續(xù)觸發(fā)帶來的無用調(diào)用
process: function () {
clearTimeout(this.timeoutId); // 先清除之前的延時琅摩,并在下面重新開始計算時間
var that = this; // 我們需要保存作用域,因為下面的setTimeout的作用域是在window躁劣,調(diào)用不要我們需要執(zhí)行的this.performProcessiong方法
this.timeoutId = setTimeout(function () { // 100毫秒以后執(zhí)行performProcessiong方法
that.performProcessiong();
}, 100) // 如果還沒有執(zhí)行就又被觸發(fā)迫吐,會根據(jù)上面的clearTimeout來清除并重新開始計算
}
};
// 嘗試開始執(zhí)行
processor.process(); // 需要重新綁定在一個觸發(fā)條件里
上面這段代碼就是最基本的實現(xiàn)方式,包在一個對象中账忘,然后在對象中互相調(diào)用志膀,里面的注釋應(yīng)該可以很清楚的說明每一步是干什么呢,最下面的processor.process()
我們在實際使用的時候肯定是需要綁定在一個觸發(fā)條件上的鳖擒,比如之前的上電梯問題上溉浙,我們就需要把processor.process()
方法綁定在增加人數(shù)的里面,這樣才會有多次調(diào)用的情況發(fā)生
上面再怎么說都是很簡單的實現(xiàn)蒋荚,在實際生產(chǎn)環(huán)境中戳稽,邏輯會相對復(fù)雜很多,但是萬變不離其宗期升,參透了最基礎(chǔ)的惊奇,再舉一反三就不是什么問題了
應(yīng)該叫“前搖”?播赁?
具體我也不知道應(yīng)該叫啥颂郎,英文叫“Leading edge”,甭管中文叫啥了容为,知道是什么意思就行了乓序。之前我們寫的代碼很明顯可以看出來,在我們連續(xù)觸發(fā)一個方法的時候坎背,是在setTimeout
結(jié)束后才去真正執(zhí)行替劈,但是還有一種情況,那就是我們在連續(xù)觸發(fā)一個方法的時候得滤,第一次觸發(fā)就執(zhí)行了陨献,然后后面的連續(xù)觸發(fā)不再執(zhí)行,等連續(xù)觸發(fā)停止耿戚,經(jīng)過延時以后湿故,再次觸發(fā)才會真正執(zhí)行阿趁。
我還是盜圖吧膜蛔。坛猪。。普遍的形式是下面這種
連續(xù)觸發(fā)結(jié)束時執(zhí)行皂股,而我們現(xiàn)在說的“前搖”則是下面這種情況
在連續(xù)觸發(fā)的一開始就執(zhí)行了墅茉,然后往后的連續(xù)觸發(fā)不執(zhí)行,連續(xù)觸發(fā)停止后再經(jīng)過延時時間后觸發(fā)才會再次執(zhí)行
下面是我自己寫的呜呐,大概意思是這樣就斤,代碼實現(xiàn)也貼出來
<!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>
</style>
</head>
<body>
<button id="addBtn">電梯上人,人數(shù)+1</button><button id="resetBtn">重置</button>
<p id="personNum">電梯人數(shù):0(假設(shè)電梯可以無限裝人)</p>
<script>
var personNum = 0; // 電梯人數(shù)
var okNext = true; // 是否可進(jìn)行下次執(zhí)行
var timeoutFn = null;
var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
/**
* @method 電梯添加人數(shù)
* @description 電梯可以上人蘑辑,但是上人以后就不能再上了洋机,不管怎么觸發(fā)都不行,除非停止觸發(fā)500毫秒以后洋魂,再觸發(fā)的時候才可以繼續(xù)執(zhí)行
*/
function addPerson() {
if (okNext) {
okNext = false;
personNum ++
personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
}
clearTimeout(timeoutFn);
timeoutFn = setTimeout(function () {
okNext = true;
}, 500)
}
/**
* @method 重置
*/
function reset() {
personNum = 0;
personNumP.innerHTML = '電梯人數(shù):0(假設(shè)電梯可以無限裝人)';
}
addBtn.addEventListener('click', addPerson);
resetBtn.addEventListener('click', reset);
</script>
</body>
</html>
上面代碼要是看不太明白绷旗,可以直接粘下去自己執(zhí)行以下看看是什么感覺,就知道是什么意思了副砍。
代碼純我自己寫的衔肢,要是有不對的地方,請大佬指正啊
Throttle 節(jié)流
什么是節(jié)流
節(jié)流呢豁翎,也是我自己的理解角骤,在連續(xù)觸發(fā)一個方法的某一時間段中,控制方法的執(zhí)行次數(shù)心剥。
同樣舉個例子吧邦尊,一個地鐵進(jìn)站閘口,10秒進(jìn)一個人(10秒內(nèi)執(zhí)行一個方法)优烧,管這10秒中來了是5個人蝉揍、10個人還是20個人,都只是進(jìn)一個人(從第一次觸發(fā)后10秒不管被觸發(fā)多少次都不會執(zhí)行匙隔,直到下一個10秒才會再執(zhí)行)疑苫。
如何實現(xiàn)呢?纷责?
時間戳
我們首先用時間戳來判斷前后的時間間隔捍掺,然后就可以知道我從上次執(zhí)行完這個方法過了多久,過了這么長時間再膳,是不是已經(jīng)超過了自己規(guī)定的時長挺勿,如果時長超過了,我就可以再次執(zhí)行了
<!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>地鐵進(jìn)站</title>
</head>
<body>
<button id="addBtn">進(jìn)站人數(shù)+1</button><button id="resetBtn">重置</button>
<p id="personTotal">旅客總?cè)藬?shù):0</p>
<p id="personNum">進(jìn)站人數(shù):0</p>
<script>
var personNum = 0; // 進(jìn)站人數(shù)
var personTotal = 0; // 一共來了多少人
var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
var personTotalP = document.getElementById('personTotal'); // 獲取顯示總?cè)藬?shù)的標(biāo)簽
var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
/**
* @method 增加進(jìn)站人數(shù)
* @description 每個時間間隔執(zhí)行的方法
*/
function addPerson() {
personNum ++;
personNumP.innerHTML = `進(jìn)站人數(shù):${personNum}`;
}
/**
* @method 節(jié)流方法(時間戳)
* @param {Function} fn 需要節(jié)流的實際方法
* @param {Number} wait 需要控制的時間長度
* @description 根據(jù)上一次執(zhí)行的時間喂柒,和這一次執(zhí)行的時間做比較不瓶,如果大于控制的時間禾嫉,就可以執(zhí)行
*/
function throttle(fn, wait) {
var prev = 0; // 第一次執(zhí)行的時候是0,所以第一次點擊的時候肯定大于這個數(shù)蚊丐,所以會立馬執(zhí)行
return function () {
var context = this;
var args = arguments;
var now = Date.now(); // 實際執(zhí)行的時間
personTotal ++;
personTotalP.innerHTML = `旅客總?cè)藬?shù):${personTotal}`;
if (now - prev >= wait) { // 執(zhí)行的時間是不是比上次執(zhí)行的時間大于需要延遲的時間熙参,大于,我們就執(zhí)行
fn.apply(context, args);
prev = now; // 執(zhí)行了以后麦备,重置上一次執(zhí)行的時間為剛剛執(zhí)行這次函數(shù)的時間孽椰,下次執(zhí)行就用這個時間為基準(zhǔn)
}
}
}
/**
* @method 重置
*/
function reset() {
personNum = 0;
personTotal = 0;
personNumP.innerHTML = '進(jìn)站人數(shù):0';
personTotalP.innerHTML = `旅客總?cè)藬?shù):0`;
}
addBtn.addEventListener('click', throttle(addPerson, 1000));
resetBtn.addEventListener('click', reset);
</script>
</body>
</html>
節(jié)流函數(shù)throttle
用到了作用域,call凛篙、apply和閉包等相關(guān)的知識黍匾,看不懂的可以看我之前的文章
上面的代碼中我感覺可以很直觀的看出來是根據(jù)判斷前后兩次的時間呛梆,來得知可不可以進(jìn)行下一次函數(shù)的執(zhí)行锐涯。參考著代碼中的注釋我覺得應(yīng)該可以看明白吧??????
setTimeout
如果我們用setTimeout
的話,我們只需要更改一下throttle
方法
/**
* @method 節(jié)流方法(setTimeout)
* @param {Function} fn 需要節(jié)流的實際方法
* @param {Number} wait 需要控制的時間長度
* @description 這個方法就很類似防抖了填物,就是判斷當(dāng)前函數(shù)有沒有延遲setTimeout函數(shù)纹腌,有的話就不執(zhí)行了
*/
function throttle(fn, wait) {
var timeout = null;
return function () {
var context = this;
var args = arguments;
personTotal ++;
personTotalP.innerHTML = `旅客總?cè)藬?shù):${personTotal}`;
if (!timeout) {
var that = this;
timeout = setTimeout(() => {
timeout = null;
fn.apply(context, args)
}, wait)
}
}
}
雖然我們只需要更改幾行代碼就實現(xiàn)了用setTimeout
實現(xiàn)節(jié)流的這個方法,但是我們仔細(xì)看上面的圖融痛,我們可以發(fā)現(xiàn)壶笼,當(dāng)我點擊第一次的時候,進(jìn)站旅客是沒有增加的雁刷,這跟我們實際情況不一樣覆劈,我們先來的,我不用等啊沛励,我直接就能進(jìn)站责语,對不對。還有當(dāng)我結(jié)束增加人數(shù)的時候目派,進(jìn)站旅客過去等待時間以后還會加一個人坤候,這當(dāng)然也不是我們想看到的。
使用時間戳還是setTimeout企蹭,取決于業(yè)務(wù)場景了
rAF(requestAnimationFrame)
誒白筹??rAF是什么谅摄?什么是requestAnimationFrame徒河?這在我沒有寫這篇博客的時候,我根本不知道window下還有個這個方法送漠,神奇吧顽照,那這個方法是干什么的呢?闽寡?
告訴瀏覽器——你希望執(zhí)行一個動畫代兵,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫尼酿。該方法需要傳入一個回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會在瀏覽器下一次重繪之前執(zhí)行植影∩亚妫————《MDN Web Docs》
就是在用這個可以一直重繪動畫,然后讓人看起來是個動畫何乎,重繪的這個過程是個很頻繁的操作句惯,所以如果我們自己寫土辩,不加以干涉支救,在性能和資源上會造成嚴(yán)重的浪費(fèi),所以我們可以使用requestAnimationFrame來使用我們的動畫看起來很流暢拷淘,又不會頻繁調(diào)用
優(yōu)點
- 目標(biāo)是60fps(16毫秒的一幀)各墨,瀏覽器將決定如何安排渲染的最佳時間。
- 相對簡單和標(biāo)準(zhǔn)的API启涯,未來不會改變贬堵,減少維護(hù)成本。
缺點
- rAF是內(nèi)部api结洼,所以我們并不方便修改
- 如果瀏覽器選項卡沒有激活黎做,就用不了
- 兼容性不好,在IE9松忍,Opera Mini和舊Android中仍然不支持
- node中不能使用
讓我們來使用rAF吧
直接上圖
<!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>rAF使用</title>
<style>
#SomeElementYouWantToAnimate {
width: 100px;
height: 100px;
background-color: #000;
}
</style>
</head>
<body>
<div id="SomeElementYouWantToAnimate"></div>
<script>
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
/**
* @method 移動我們的小黑方塊
*/
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
</script>
</body>
</html>
總結(jié)
rAF是一個內(nèi)部api蒸殿,固定的16毫秒執(zhí)行一次,因為人眼接受60fps的動畫就會感到很流暢了鸣峭,如果我們需要改變rAF的執(zhí)行時間宏所,那我們只能自己去寫動畫的方法,節(jié)流還是防抖摊溶,看個人愛好了
收官
防抖:連續(xù)觸發(fā)一個函數(shù)爬骤,不管是觸發(fā)開始執(zhí)行還是結(jié)束執(zhí)行,只要在連續(xù)觸發(fā)莫换,就只執(zhí)行一次
節(jié)流:規(guī)定時間內(nèi)只執(zhí)行一次霞玄,不管是規(guī)定時間內(nèi)被觸發(fā)了多少次
rAF:也算是一種節(jié)流手段,原生api拉岁,旨在使動畫在盡量少占用資源的情況下使動畫流暢
End Game
《復(fù)仇者聯(lián)盟4》現(xiàn)階段的漫威宇宙的結(jié)束坷剧,《哈利·波特》《火影忍者》一個個完結(jié)的電影,雖然在時刻提醒著我們青春再慢慢的消失膛薛,正如英雄聯(lián)盟中的那句話听隐,我們有了新的敵人叫“生活”。當(dāng)這些完結(jié)的并不是真正的結(jié)束哄啄,《哈利·波特》有《神奇動物在哪里》雅任,《火影忍者》有《博人傳》风范,《鋼鐵俠》有《蜘蛛俠》,晚輩從前輩手中接過接力棒沪么,繼續(xù)往后跑硼婿,我們也從自己青蔥的歲月進(jìn)入下一階段,努力奮斗吧G莩怠寇漫!