節(jié)流(throttle)與防抖(debounce)
場(chǎng)景
因頻繁執(zhí)行DOM操作,資源加載等行為哗总,導(dǎo)致UI停頓甚至瀏覽器崩潰。
- window對(duì)象頻繁的onresize,onscroll等事件
- 拖拽的mousemove事件
- 射擊游戲的mousedown灯变,keydown事件
- 文字輸入敬肚,自動(dòng)完成的keyup事件
比如每次mouseover就會(huì)觸發(fā)一次函數(shù),又比如每次搜索一下就會(huì)向服務(wù)器發(fā)送一個(gè)請(qǐng)求倾贰,這樣既沒有意義冕碟,也很浪費(fèi)資源。
解決方案
實(shí)際上對(duì)于window和resize事件匆浙,實(shí)際需求大多為停止改變大小n毫秒后執(zhí)行后續(xù)處理安寺;而其他事件大多數(shù)的需求是以一定的頻率執(zhí)行后續(xù)處理。針對(duì)這兩種需求出現(xiàn)了debounce(函數(shù)去抖)和throttle(函數(shù)節(jié)流)兩種方式首尼。
節(jié)流與防抖:
節(jié)流
比如mouseover挑庶,resize這種事件言秸,每當(dāng)有變化的時(shí)候,就會(huì)觸發(fā)一次函數(shù)迎捺,這樣很浪費(fèi)資源举畸。就比如一個(gè)持續(xù)流水的水龍頭,水龍頭開到最大的時(shí)候很浪費(fèi)水資源凳枝,將水龍頭開得小一點(diǎn)抄沮,讓他每隔200毫秒流出一滴水,這樣能源源不斷的流出水而又不浪費(fèi)范舀。而節(jié)流就是每隔n的時(shí)間調(diào)用一次函數(shù)合是,而不是一觸發(fā)事件就調(diào)用一次,這樣就會(huì)減少資源浪費(fèi)锭环。
防抖
A和B說話聪全,A一直bbbbbb,當(dāng)A持續(xù)說了一段時(shí)間的話后停止講話辅辩,過了10秒之后难礼,我們判定A講完了,B開始回答A的話玫锋;如果10秒內(nèi)A又繼續(xù)講話蛾茉,那么我們判定A沒講完,B不響應(yīng)撩鹿,等A再次停止后谦炬,我們?cè)俅斡?jì)算停止的時(shí)間,如果超過10秒B響應(yīng)节沦,如果沒有則B不響應(yīng)键思。
節(jié)流與防抖的區(qū)別
節(jié)流與防抖的前提都是某個(gè)行為持續(xù)地觸發(fā),不同之處只要判斷是要優(yōu)化到減少它的執(zhí)行次數(shù)還是只執(zhí)行一次就行甫贯。
節(jié)流例子吼鳞,像dom的拖拽,如果用消抖的話叫搁,就會(huì)出現(xiàn)卡頓的感覺赔桌,因?yàn)橹辉谕V沟臅r(shí)候執(zhí)行了一次,這個(gè)時(shí)候就應(yīng)該用節(jié)流渴逻,在一定時(shí)間內(nèi)多次執(zhí)行疾党,會(huì)流暢很多。
防抖例子惨奕,像仿百度搜索雪位,就應(yīng)該用防抖,當(dāng)我連續(xù)不斷輸入時(shí)墓贿,不會(huì)發(fā)送請(qǐng)求茧泪;當(dāng)我一段時(shí)間內(nèi)不輸入了蜓氨,才會(huì)發(fā)送一次請(qǐng)求;如果小于這段時(shí)間繼續(xù)輸入的話队伟,時(shí)間會(huì)重新計(jì)算穴吹,也不會(huì)發(fā)送請(qǐng)求。
debounce(防抖)
防抖分為立即防抖
和非立即防抖
最常見的例子就是:搜索
非立即防抖:觸發(fā)事件后函數(shù)不會(huì)立即執(zhí)行嗜侮,而是在n秒之后執(zhí)行港令,如果n秒之內(nèi)又觸發(fā)了事件,則會(huì)重新計(jì)算函數(shù)執(zhí)行時(shí)間锈颗。
立即防抖:觸發(fā)事件后函數(shù)會(huì)立即執(zhí)行顷霹,然后n秒內(nèi)不觸發(fā)事件才會(huì)執(zhí)行函數(shù)的效果
非立即防抖
function debounce(func, wait) {
var timeout = null;
var context = this;
var args = arguments;
return function () {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
立即防抖
function debounce(func, wait) {
var timeout = null;
var context = this;
var args = arguments;
return function () {
if (timeout) clearTimeout(timeout);
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
也可以將非立即執(zhí)行版和立即執(zhí)行版的防抖函數(shù)結(jié)合起來,實(shí)現(xiàn)最終的雙劍合璧版的防抖函數(shù)击吱。
/**
* @desc 函數(shù)防抖
* @param func (function) 函數(shù)
* @param wait (number) 延遲執(zhí)行毫秒數(shù)
* @param immediate (boolean) true 表立即執(zhí)行淋淀,false 表非立即執(zhí)行
*/
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
大神代碼
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function (func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArgs(function (args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
throttle(節(jié)流)
節(jié)流分為時(shí)間戳
和定時(shí)版本
如果一個(gè)函數(shù)持續(xù)的,頻繁的觸發(fā)覆醇,那么就讓他在一定的時(shí)間間隔后觸發(fā)朵纷。
高頻事件:
onscroll oninput resize onkeyup onkeydown onkerpress
onkeyup:每鍵入一個(gè)字母觸發(fā)一次(并不是按照我們輸入的漢字計(jì)算的)
節(jié)流單純的降低代碼執(zhí)行的頻率,保證一段時(shí)間內(nèi)核心代碼只執(zhí)行一次永脓。
時(shí)間戳版和定時(shí)器版的節(jié)流函數(shù)的區(qū)別就是袍辞,時(shí)間戳版的函數(shù)觸發(fā)是在時(shí)間段內(nèi)開始的時(shí)候,而定時(shí)器版的函數(shù)觸發(fā)是在時(shí)間段內(nèi)結(jié)束的時(shí)候常摧。
時(shí)間戳版
function throttle(func, wait) {
var previous = 0;
return function () {
var now = Date.now();
var context = this;
var args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
定時(shí)器版本
function throttle(func, wait) {
var timeout;
return function() {
var context = this;
var args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
可以將時(shí)間戳版和定時(shí)器版的節(jié)流函數(shù)結(jié)合起來搅吁,實(shí)現(xiàn)雙劍合璧版的節(jié)流函數(shù)。
/**
* @desc 函數(shù)節(jié)流
* @param func (function) 函數(shù)
* @param wait (number) 延遲執(zhí)行毫秒數(shù)
* @param type (number) 1 表時(shí)間戳版落午,2 表定時(shí)器版
*/
function throttle(func, wait ,type) {
if(type===1){
var previous = 0;
}else if(type===2){
var timeout;
}
return function() {
var context = this;
var args = arguments;
if(type===1){
var now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
- 大神代碼
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function (func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function () {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null; //顯示地釋放內(nèi)存谎懦,防止內(nèi)存泄漏
};
var throttled = function () {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
總結(jié)
throttle和debounce均是通過減少實(shí)際邏輯處理過程的執(zhí)行來提高事件處理函數(shù)運(yùn)行性能的手段,并沒有實(shí)質(zhì)上減少事件的觸發(fā)次數(shù)板甘。比如說党瓮,我搜索時(shí)详炬,onkeyup該幾次還是幾次盐类,只是我的請(qǐng)求變少了,處理的邏輯少了呛谜,從而提高了性能在跳。
1、節(jié)流(throttle): 創(chuàng)建一個(gè)節(jié)流函數(shù)隐岛,在等待時(shí)間內(nèi)最多執(zhí)行 一次的函數(shù)
2猫妙、防抖(debounce):創(chuàng)建一個(gè) debounced(防抖動(dòng))函數(shù),該函數(shù)會(huì)從上一次被調(diào)用后聚凹,延遲多少時(shí)間后調(diào)用方法割坠,如果不停執(zhí)行函數(shù)齐帚,執(zhí)行時(shí)間被覆蓋
案例
<template>
<div>
<button @click="throttleFun">點(diǎn)擊按鈕(節(jié)流)</button>
<input type="text" @keyup="debounceFun" />
</div>
</template>
<script>
// 導(dǎo)入lodash 函數(shù)function段
import funHelper from 'lodash/function'
export default {
methods: {
// 防抖(延遲多少時(shí)間調(diào)用,如果一直keyup則會(huì)覆蓋之前的時(shí)間重新計(jì)算)
debounceFun: funHelper.debounce((e)=>{
console.log(e.target.value);
}, 2000),
// 2秒內(nèi)調(diào)用一次
// throttleFun: funHelper.throttle(()=>{
throttleFun: funHelper.throttle(function(){
// 如果使用()=> 箭頭函數(shù) this指向根實(shí)例,使用普通函數(shù)function()不改變this指向本組件
console.log(this);
console.log('2秒內(nèi)只能調(diào)用一次!');
}, 2000, { 'trailing': false }),
//
throttleFun2(){
console.log('3秒內(nèi)調(diào)用一次');
},
initFun(){
// 定義節(jié)流函數(shù)
let throttleF = funHelper.throttle(this.throttleFun2, 3000)
// 循環(huán)調(diào)用
for(let i=0;i<10;i++){
throttleF();
}
}
},
created(){
this.initFun();
}
}
</script>