防抖和節(jié)流

防抖與節(jié)流

前言

我們 JavaScript 中有一些事件,比如瀏覽器的 resize缀辩、scroll 事件肛搬,鼠標(biāo)的 mousemove蛇捌、mouseover 事件以及輸入框的 keypress、keyup 事件赫编,它們在觸發(fā)的時候會不斷調(diào)用事件綁定的回調(diào)函數(shù)巡蘸,極大的浪費(fèi)資源,降低前端性能擂送。為了優(yōu)化用戶體驗(yàn)悦荒,我們需要對這類事件進(jìn)行調(diào)用次數(shù)的限制。我們可以使用防抖與節(jié)流來降低事件的觸發(fā)頻率嘹吨。

防抖

1搬味、定義

當(dāng)連續(xù)觸發(fā)事件時,事件被觸發(fā) N 秒后才執(zhí)行回調(diào)躺苦,否則如果在這 n 秒內(nèi)又被觸發(fā)身腻,則重新計時

例:鼠標(biāo)移動事件
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動打印出無數(shù)次
防抖處理:只在鼠標(biāo)不再移動后 N 秒打印出鼠標(biāo)最后停止的位置(N 秒為人為設(shè)置)

2、為什么要防抖匹厘?

在某一函數(shù)在短時間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓嘀趟,而我們并不真正需要連續(xù)觸發(fā)這個函數(shù)。我們需要的是連續(xù)觸發(fā)中最后一次的結(jié)果愈诚,這時我們使用防抖來優(yōu)化函數(shù)的觸發(fā)她按,達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓炕柔。

3酌泰、防抖的實(shí)現(xiàn)

// 防抖
function debounce(fun, delay) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    fun.id = setTimeout(function () {
      fun.call(that, _args);
    }, delay);
  };
}

4、示例

移動鼠標(biāo)匕累,更新數(shù)字陵刹,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)

4.1 正常調(diào)用

每次移動都會調(diào)用回調(diào)。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = callback;
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
正常調(diào)用

4.2 防抖調(diào)用

每次移動間隔超過 1000ms 才會調(diào)用回調(diào)欢嘿,反之重新計算時間衰琐。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = dobounce(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
防抖實(shí)現(xiàn)

4.3 防抖優(yōu)化

首次調(diào)用立即執(zhí)行

// 防抖
function debounce(fun, delay, immediate) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    if (immediate) {
      // 如果已經(jīng)執(zhí)行過也糊,不再執(zhí)行
      const callNow = !fun.id;
      fun.id = setTimeout(function () {
        fun.id = null;
      }, delay);
      if (callNow) fun.call(that, _args);
    } else {
      fun.id = setTimeout(function () {
        fun.call(that, _args);
      }, delay);
    }
  };
}
防抖優(yōu)化

節(jié)流

1、定義

當(dāng)連續(xù)觸發(fā)事件時羡宙,每隔 N 時間狸剃,觸發(fā)一次函數(shù)

例:監(jiān)聽鼠標(biāo)在屏幕中的位置并打印,連續(xù)不斷的移動鼠標(biāo)一段時間狗热,獲取鼠標(biāo)的移動軌跡
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動打印出無數(shù)次钞馁,(軌跡最為精細(xì))
節(jié)流處理:當(dāng)我們不需要這樣精確的軌跡時,每隔 N 秒觸發(fā)一次函數(shù)打印鼠標(biāo)位置匿刮,得到較為粗糙的鼠標(biāo)軌跡(N 秒為人為設(shè)置)

2僧凰、為什么要節(jié)流?

同樣的僻焚,也是為了防止在某一函數(shù)在短時間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓允悦,而我們并不真正需要連續(xù)觸發(fā)這個函數(shù)。我們需要的是穩(wěn)定的每隔一段時間觸發(fā)一次虑啤,這時我們使用節(jié)流來優(yōu)化函數(shù)的觸發(fā)隙弛,達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓狞山。

3全闷、節(jié)流的實(shí)現(xiàn)

// 節(jié)流
function throttle(fun, delay) {
  let last, deferTimer;
  return function (args) {
    let that = this;
    let _args = arguments;
    let now = +new Date();
    // 保證第一次執(zhí)行
    if (last && now < last + delay) {
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fun.apply(that, _args);
      }, delay);
    } else {
      last = now;
      fun.apply(that, _args);
    }
  };
}

4、示例

移動鼠標(biāo)萍启,更新數(shù)字婚被,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)

4.1 節(jié)流調(diào)用

首次調(diào)用立即執(zhí)行永罚,每隔 1000ms 調(diào)用一次吊说。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = throttle(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
節(jié)流調(diào)用

附加

lodash 的防抖與節(jié)流實(shí)現(xiàn)

// 防抖
function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  wait = toNumber(wait) || 0;
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = "maxWait" in options;
    maxWait = maxing
      ? nativeMax(toNumber(options.maxWait) || 0, wait)
      : maxWait;
    trailing = "trailing" in options ? !!options.trailing : trailing;
  }

  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;

    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  function leadingEdge(time) {
    lastInvokeTime = time;
    timerId = setTimeout(timerExpired, wait);
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

// 節(jié)流
function throttle(func, wait, options) {
  var leading = true,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (isObject(options)) {
    leading = "leading" in options ? !!options.leading : leading;
    trailing = "trailing" in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    leading: leading,
    maxWait: wait,
    trailing: trailing,
  });
}

參考鏈接

lodash 的實(shí)現(xiàn)
可視化防抖與節(jié)流
防抖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末状植,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子驳遵,更是在濱河造成了極大的恐慌淫奔,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堤结,死亡現(xiàn)場離奇詭異唆迁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)竞穷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門唐责,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘾带,你說我怎么就攤上這事鼠哥。” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵肴盏,是天一觀的道長科盛。 經(jīng)常有香客問我,道長菜皂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任厉萝,我火速辦了婚禮恍飘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谴垫。我一直安慰自己章母,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布翩剪。 她就那樣靜靜地躺著乳怎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪前弯。 梳的紋絲不亂的頭發(fā)上蚪缀,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機(jī)與錄音恕出,去河邊找鬼询枚。 笑死,一個胖子當(dāng)著我的面吹牛浙巫,可吹牛的內(nèi)容都是我干的金蜀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼的畴,長吁一口氣:“原來是場噩夢啊……” “哼渊抄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丧裁,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤护桦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渣慕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘶炭,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年逊桦,在試婚紗的時候發(fā)現(xiàn)自己被綠了眨猎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡强经,死狀恐怖睡陪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤兰迫,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布信殊,位于F島的核電站,受9級特大地震影響汁果,放射性物質(zhì)發(fā)生泄漏涡拘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一据德、第九天 我趴在偏房一處隱蔽的房頂上張望鳄乏。 院中可真熱鬧,春花似錦棘利、人聲如沸橱野。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽水援。三九已至,卻和暖如春茅郎,著一層夾襖步出監(jiān)牢的瞬間蜗元,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工只洒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留许帐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓毕谴,卻偏偏與公主長得像成畦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涝开,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容