一鞋仍、緩沖
緩沖家妆,這是一個(gè)非常普遍的概念珊豹,凡是使某種事物進(jìn)行減慢或減弱變化過(guò)程都可以叫緩沖簸呈。網(wǎng)頁(yè)交互中也需要緩沖,也就是 防抖(debounce)
和 節(jié)流(throttle)
—— 讓高頻事件的反應(yīng)慢一點(diǎn)店茶,雖然是老生常談蜕便,但也不容忽視。
Lodash 中已早有實(shí)現(xiàn)忽妒。
-
debounce
:一個(gè)事件被觸發(fā)后玩裙,立即設(shè)定一個(gè)小延時(shí)。如果在延時(shí)期間事件再次被觸發(fā)段直,則銷(xiāo)毀前一個(gè)延時(shí)并重新開(kāi)始延時(shí)吃溅;否則,開(kāi)始執(zhí)行處理函數(shù)鸯檬。簡(jiǎn)而言之决侈,就是不斷地重新設(shè)置計(jì)時(shí)器。 舉例:電梯門(mén)喧务,若干秒內(nèi)無(wú)人進(jìn)入赖歌,則自動(dòng)關(guān)門(mén)。 -
throttle
:在事件連續(xù)觸發(fā)的過(guò)程中功茴,每隔一段時(shí)間執(zhí)行一次處理函數(shù)庐冯。舉例:文章自動(dòng)保存,在編輯過(guò)程中每隔幾分鐘自動(dòng)保存一次坎穿。
二展父、Debounce
1. debounce基本實(shí)現(xiàn)
-
需求
:鼠標(biāo)在頁(yè)面上移動(dòng),當(dāng)移動(dòng)停止 2 秒后玲昧,提示“鼠標(biāo)移動(dòng)了”栖茉。 -
實(shí)現(xiàn)
:根據(jù)前面的定義,在鼠標(biāo)移動(dòng)時(shí)設(shè)置新的計(jì)時(shí)器即可:
let timer = null;
function setTimer(){
timer && clearTimeout(timer);
timer = setTimeout(function () {
console.log("鼠標(biāo)移動(dòng)了");
}, 2000)
}
window.onmousemove = setTimer;
2. 封裝
-
需求
:由于以上代碼使用頻繁孵延,需要封裝成一個(gè)便捷的工具函數(shù)吕漂,參數(shù)為事件處理函數(shù)和等待時(shí)長(zhǎng)。 -
實(shí)現(xiàn)
:實(shí)參不一定會(huì)嚴(yán)格按照形參來(lái)傳尘应,注意防御
/**
* debounce函數(shù) V1.0
* @param {function} fn 事件處理函數(shù)惶凝,默認(rèn)為一個(gè)空函數(shù)
* @param {number} delay 延時(shí)時(shí)間,單位為毫秒
* @return {function} resetTimer 返回一個(gè)可以重新設(shè)定計(jì)時(shí)器的函數(shù)
*/
function debounce(fn=function(){}, delay=0){
let timer = null;
function resetTimer() {
if(timer) {clearTimeout(timer)}
// 要保證參數(shù)delay是自然數(shù)
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
timer = setTimeout(fn, delayTime);
}
return resetTimer;
}
function onMove() { console.log("鼠標(biāo)移動(dòng)了") }
window.onmousemove = debounce(onMove, 2000);
3. 執(zhí)行環(huán)境:this
在事件處理函數(shù)中經(jīng)常需要訪問(wèn)this對(duì)象犬钢。 上面的debounce函數(shù)被用在了window.onmousemove
上梨睁,所以onMove里的this肯定指的是window。現(xiàn)在我們把debounce用在別的對(duì)象上娜饵,看看this的指向會(huì)不會(huì)變化:
// ...debounce函數(shù) V1.0
//假設(shè)有個(gè)ID為menu的元素
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:window對(duì)象
出問(wèn)題了,onBtnHover里的 this 并不是oBtn官辈,問(wèn)題在debounce函數(shù)的setTimeout里:
setTimeout(fn, delayTime); //相當(dāng)于
function callback(){
fn();
}
setTimeout(callback, delayTime);
這里的 fn 并沒(méi)有與任何對(duì)象綁定箱舞,所以它屬于window對(duì)象遍坟,因此this指向window。
4. 解決this指向問(wèn)題
ES5:
一個(gè)函數(shù)在被調(diào)用時(shí)晴股,會(huì)自動(dòng)取得兩個(gè)特殊變量:this和arguments愿伴。在全局環(huán)境調(diào)用函數(shù)時(shí),this代表window對(duì)象电湘;而當(dāng)函數(shù)被作為其他某個(gè)對(duì)象的方法而調(diào)用時(shí)隔节,this就代表那個(gè)對(duì)象。
簡(jiǎn)而言之:
ES5的函數(shù)中寂呛,this是執(zhí)行時(shí)函數(shù)所在的那個(gè)對(duì)象怎诫。
ES6:
this是函數(shù)定義所在的那個(gè)對(duì)象。
在下面的代碼中贷痪,debounce為oBtn.onmousemove綁定了事件處理函數(shù)resetTimer幻妓。根據(jù)ES5中this的性質(zhì),該函數(shù)運(yùn)行時(shí)劫拢,它的執(zhí)行環(huán)境必然是調(diào)用者:
function debounce(fn=function(){}, delay=0){
function resetTimer() {
console.log("resetTimer執(zhí)行環(huán)境:", this);
}
return resetTimer;
}
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log(this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:執(zhí)行環(huán)境是oBtn
因此肉津,應(yīng)該在resetTimer函數(shù)里將 this 保存下來(lái),然后在setTimeout中使用call舱沧、apply函數(shù)修改this指向妹沙。debounce函數(shù)可以這樣修改:
/**
* debounce函數(shù) V2.0
* @param {function} fn 事件處理函數(shù),默認(rèn)為一個(gè)空函數(shù)
* @param {number} delay 延時(shí)時(shí)間熟吏,單位為毫秒
* @return {function} resetTimer 返回一個(gè)可以重新設(shè)定計(jì)時(shí)器的函數(shù)
*/
function debounce(fn=function(){}, delay=0){
let timer = null;
function resetTimer() {
const context = this; /**** 修改:保存執(zhí)行環(huán)境 ****/
if(timer) {clearTimeout(timer)}
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
timer = setTimeout(
function(){fn.apply(context)}, /**** 修改:應(yīng)用執(zhí)行環(huán)境 ****/
delayTime
);
}
return resetTimer;
}
const oBtn = document.getElementById("menu");
function onBtnHover() {console.log("鼠標(biāo)移動(dòng)了距糖,執(zhí)行環(huán)境是:", this)};
oBtn.onmousemove = debounce(onBtnHover, 1000);
// 結(jié)果:執(zhí)行環(huán)境是 oBtn
5. 參數(shù)傳遞
上面的代碼保證了this指向正確,但是一旦試圖為onBtnHover傳參分俯,就會(huì)影響到this指向肾筐。
-
需求
:onBtnHover函數(shù)打印的是為其傳遞的字符串 -
實(shí)現(xiàn)
:用一個(gè)匿名函數(shù)調(diào)用onBtnHover并傳參
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){onBtnHover("鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:")},
1000
);
// 結(jié)果:執(zhí)行環(huán)境是 window
-
原因
: 為了傳參缸剪,我們用一個(gè)匿名函數(shù)來(lái)調(diào)用onBtnHover吗铐,因此apply(context)
只是修改了這個(gè)匿名函數(shù)的this,而onBtnHover的this仍然未被修改杏节。 -
證明
:在這個(gè)匿名函數(shù)內(nèi)打印一下this
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){
console.log("匿名函數(shù)執(zhí)行環(huán)境:", this); /*** 修改 ***/
onBtnHover("鼠標(biāo)移動(dòng)了唬渗,執(zhí)行環(huán)境是:");
},
1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn
// 鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:window
-
解決方法1
: 在匿名函數(shù)里使用call奋渔、apply修改onBtnHover的this镊逝,并傳參
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
function onBtnHover(text) {console.log(text, this)};
oBtn.onmousemove = debounce(
function(){
console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
onBtnHover.call(this, "鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:"); /*** 修改 ***/
},
1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn
// 鼠標(biāo)移動(dòng)了嫉鲸,執(zhí)行環(huán)境是:oBtn
注意:以上代碼中如果使用apply撑蒜,則參數(shù)必須以數(shù)組的形式傳遞:onBtnHover.apply(this, ["鼠標(biāo)移動(dòng)了,執(zhí)行環(huán)境是:"])
call()
方法與apply()
方法的作用相同,它們的區(qū)別僅在于接收參數(shù)的方式不同座菠。前者需要將參數(shù)列舉出來(lái)狸眼,后者則需要將參數(shù)放到數(shù)組里。
-
解決方法2
:直接將onBtnHover定義為oBtn的一個(gè)方法
// ...debounce函數(shù) V2.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text) {console.log(text, this)}; /*** 修改 ***/
oBtn.onmousemove = debounce(
function(){
console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
oBtn.onBtnHover("鼠標(biāo)移動(dòng)了浴滴,執(zhí)行環(huán)境是:"); /*** 修改 ***/
},
1000
);
// 匿名函數(shù)執(zhí)行環(huán)境:oBtn指向的元素
// 鼠標(biāo)移動(dòng)了拓萌,執(zhí)行環(huán)境是:oBtn指向的元素
6. Debounce:總結(jié)
Debounce是在綁定事件處理函數(shù)時(shí)使用的,它會(huì)為事件綁定一個(gè)不斷重新設(shè)定計(jì)時(shí)器的函數(shù)升略。使用上面的debounce函數(shù) V2.0 時(shí)需要向其傳遞真正的事件處理函數(shù)以及延時(shí)時(shí)間微王。 如果需要給事件處理函數(shù)傳參,則要用上述的兩種方式來(lái)控制this指向品嚣。
三炕倘、Throttle
節(jié)流與防抖的差別僅僅是控制方式不同,所以我們可以借鑒debounce的實(shí)現(xiàn)思路腰根。
1. throttle基本實(shí)現(xiàn)
-
需求
:在鼠標(biāo)移動(dòng)的過(guò)程中激才,每隔1秒打印一次“鼠標(biāo)在移動(dòng)” -
實(shí)現(xiàn)
:鼠標(biāo)剛移動(dòng)就會(huì)設(shè)定一個(gè)計(jì)時(shí)器,同時(shí)設(shè)定一個(gè)標(biāo)記额嘿,表明在計(jì)時(shí)期間若鼠標(biāo)再次移動(dòng)瘸恼,則什么都不做,計(jì)時(shí)結(jié)束后執(zhí)行處理函數(shù)册养,同時(shí)清除標(biāo)記东帅。這樣一來(lái),當(dāng)再次移動(dòng)鼠標(biāo)時(shí)球拦,新的計(jì)時(shí)器和標(biāo)記就會(huì)被設(shè)定……
/**
* throttle函數(shù) V1.0
* @param {function} fn 事件處理函數(shù)靠闭,默認(rèn)為一個(gè)空函數(shù)
* @param {number} delay 延時(shí)時(shí)間,單位為毫秒
* @return {function} setNewTimer 返回一個(gè)可以按時(shí)間間隔設(shè)定計(jì)時(shí)器的函數(shù)
*/
function throttle(fn = function(){}, delay = 0){
let isTiming = false; // 是否正在計(jì)時(shí)
let timer = null;
function setNewTimer(){
if(isTiming) {return};
//開(kāi)始設(shè)置計(jì)時(shí)器和標(biāo)記
isTiming = true; // 設(shè)定標(biāo)記坎炼,阻止下次觸發(fā)
const delayTime = isNaN(parseInt(delay)) ?
0 : Math.abs(parseInt(delay));
let context = this;
if(timer){clearTimeout(timer)}; //清除已有計(jì)時(shí)器
timer = setTimeout(function(){
fn.apply(context);
isTiming = false;
}, delayTime);
}
return setNewTimer;
}
function onMove(){console.log( "鼠標(biāo)在移動(dòng)" )};
window.onmousemove = throttle(onMove, 1000);
2. 傳參與this指向
-
需求
:鼠標(biāo)在指定的元素上移動(dòng)時(shí)每隔1秒打印一次“鼠標(biāo)在移動(dòng)”愧膀,并且保證this指向該元素。 -
實(shí)現(xiàn)
:使用前面提到的兩種方式即可 -
方案1
:在匿名函數(shù)里使用call谣光、apply修改onBtnHover的this檩淋,并傳參
// throttle函數(shù) V1.0
const oBtn = document.getElementById("menu");
function onBtnHover(text){console.log( text, this )};
oBtn.onmousemove = throttle(
function(){
console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
onBtnHover.call(this, "鼠標(biāo)在移動(dòng),執(zhí)行環(huán)境:")
}, 1000);
-
方案2
:直接將onBtnHover定義為oBtn的一個(gè)方法
// throttle函數(shù) V1.0
const oBtn = document.getElementById("menu");
oBtn.onBtnHover = function(text){console.log( text, this )};
oBtn.onmousemove = throttle(
function(){
console.log("匿名函數(shù)執(zhí)行環(huán)境:", this);
oBtn.onBtnHover("鼠標(biāo)在移動(dòng)萄金,執(zhí)行環(huán)境:")
}, 1000);
四蟀悦、總結(jié)
至此,debounce和throttle均已實(shí)現(xiàn)完畢氧敢,我們還可以向它們添加更多參數(shù)來(lái)擴(kuò)展其功能日戈,比如用另外一個(gè)參數(shù)表示是否需要立即執(zhí)行,等等孙乖。