JavaScript-函數(shù)節(jié)流

在上一篇文章 JavaScript-函數(shù)防抖 中我們學(xué)習(xí)了什么是防抖荒典,并且一步步實(shí)現(xiàn)了防抖函數(shù),今天我們一起來學(xué)習(xí)節(jié)流(throttle)早处。

什么是節(jié)流

函數(shù)節(jié)流(throttle):當(dāng)持續(xù)觸發(fā)事件時(shí),保證一定時(shí)間段內(nèi)只調(diào)用一次事件處理函數(shù)。簡單的說,就是讓一個(gè)函數(shù)無法在很短時(shí)間間隔內(nèi)被連續(xù)調(diào)用戈毒,只有當(dāng)上一次函數(shù)執(zhí)行后過了規(guī)定的時(shí)間間隔,才能進(jìn)行下一次函數(shù)的執(zhí)行横堡。

函數(shù)節(jié)流主要有兩種實(shí)現(xiàn)方法:時(shí)間戳和定時(shí)器埋市。

歡迎關(guān)注我的微信公眾號:前端極客技術(shù)(FrontGeek)

節(jié)流的實(shí)現(xiàn)

時(shí)間戳

思路

只要觸發(fā),就用Date方法獲取當(dāng)前時(shí)間 now命贴,與上一次調(diào)用的時(shí)間 previous 作比較

  • 如果時(shí)間差大于等于規(guī)定的時(shí)間間隔道宅,就執(zhí)行一次目標(biāo)函數(shù)食听,執(zhí)行以后,將存儲上一次調(diào)用時(shí)間previous的值更新為當(dāng)前時(shí)間now

  • 如果時(shí)間差小于規(guī)定的時(shí)間間隔污茵,則等待下一次觸發(fā)重新進(jìn)行第一步操作樱报。

代碼實(shí)現(xiàn)

// delay:規(guī)定的時(shí)間間隔
function throttle(func, delay) {
  var context, args
  var previous = 0
  return function() {
    context = this
    args = arguments
    var now = +new Date()
    if (now - previous >= delay) {
      func.apply(context, args)
      previous = now
    }
  }
}

我們依舊采用防抖那篇文章用到的鼠標(biāo)移動的例子來驗(yàn)證節(jié)流,調(diào)用節(jié)流函數(shù)方式如下:

container.onmousemove = throttle(mouseMove, 2000)

效果如下:

java-throttle-1.gif

從上圖中可以看到:當(dāng)鼠標(biāo)移入時(shí)泞当,事件立即執(zhí)行迹蛤,每過2秒會執(zhí)行一次,假設(shè)在第7秒時(shí)移出襟士,停止觸發(fā)盗飒,以后不會再執(zhí)行事件。

定時(shí)器

思路

用定時(shí)器實(shí)現(xiàn)時(shí)間間隔陋桂。

  • 當(dāng)定時(shí)器不存在逆趣,說明可以執(zhí)行函數(shù),定義一個(gè)定時(shí)器來向任務(wù)隊(duì)列注冊目標(biāo)函數(shù)章喉。目標(biāo)函數(shù)執(zhí)行后設(shè)置保存定時(shí)器ID變量為空
  • 當(dāng)定時(shí)器已經(jīng)被定義汗贫,說明已經(jīng)在等待過程中,則等待下次觸發(fā)事件時(shí)再進(jìn)行查看秸脱。

代碼實(shí)現(xiàn)

function throttle(func, delay) {
  var timeout = null
  var context, args
  return function() {
    context = this
    args = arguments
    if (!timeout) {
      timeout = setTimeout(function() {
        timeout = null
        func.apply(context, args)
      }, delay)
    }
  }
}

代碼執(zhí)行效果如下:


java-throttle-2.gif

從上面的動圖我們可以看到:鼠標(biāo)移入時(shí)落包,事件不會立即執(zhí)行,之后每隔2秒執(zhí)行一次摊唇,假設(shè)在第5秒時(shí)移出咐蝇,事件停止觸發(fā),但在第6秒時(shí)依舊會執(zhí)行一次事件巷查。

兩者區(qū)別:

  • 時(shí)間戳實(shí)現(xiàn):觸發(fā)事件一發(fā)生先執(zhí)行目標(biāo)函數(shù)有序,然后再等待規(guī)定的時(shí)間間隔再次執(zhí)行目標(biāo)函數(shù)。如果在等待過程中停止觸發(fā)岛请,后續(xù)不會再執(zhí)行目標(biāo)函數(shù)旭寿。
  • 定時(shí)器實(shí)現(xiàn):觸發(fā)事件一發(fā)生,先等待夠規(guī)定的時(shí)間間隔再執(zhí)行目標(biāo)函數(shù)崇败。即使在等待過程中停止觸發(fā)盅称,若定時(shí)器已經(jīng)在任務(wù)隊(duì)列里注冊了定時(shí)器,也會執(zhí)行最后一次后室。

強(qiáng)強(qiáng)聯(lián)合:時(shí)間戳+定時(shí)器

如果我們想要能夠控制鼠標(biāo)移入能夠立即執(zhí)行缩膝,停止觸發(fā)的時(shí)候能夠再執(zhí)行一次,我們可以綜合時(shí)間戳和定時(shí)器兩種方法來實(shí)現(xiàn)“有頭有尾”的效果岸霹。

在這里我們需要注意:控制好在上一周期的“尾”和下一周期的“頭”之間時(shí)間間隔疾层,我們引入變量remaining表示還需要等待的時(shí)間,來讓尾部那一次的執(zhí)行也符合時(shí)間間隔贡避。

代碼實(shí)現(xiàn)

function throttle(func, delay) {
    var timeout, context, args, result
    var previous = 0
    
    var throttled = function() {
        context = this
        args = arguments
        var now = +new Date()
        // 下次觸發(fā)func剩余時(shí)間
        var remaining = delay - (now - previous)
    // 如果沒有剩余的時(shí)間了或者你改了系統(tǒng)時(shí)間
        if (remaining <= 0 || remaining > delay) {
            if (timeout) {
                clearTimeout(timeout)
                timeout = null
            }
            func.apply(context, args)
            previous = now
        } else if (!timeout) {
            timeout = setTimeout(function(){
                previous = +new Date()
                timeout = null
                func.apply(context, args)
            }, remaining)
        }
    }
  return throttled
}

代碼執(zhí)行效果如下:


java-throttle-3.gif

優(yōu)化

在上面結(jié)合時(shí)間戳和定時(shí)器的解法的基礎(chǔ)上痛黎,如果我們想實(shí)現(xiàn)是否啟用第一次 / 尾部最后一次計(jì)時(shí)回調(diào)的執(zhí)行予弧,如何實(shí)現(xiàn)?

我們可以設(shè)置個(gè)options作為第三個(gè)參數(shù)湖饱,然后根據(jù)傳的值判斷到底哪種效果桌肴,我們約定:

  • leading:false表示禁用第一次執(zhí)行
  • trailing:false表示禁用停止觸發(fā)的回調(diào)

代碼實(shí)現(xiàn)如下:

function throttle(func, delay, options) {
  var timeout, context, args, result
  var previous = 0
  if (!options) options = {}

  var later = function() {
    previous = options.leading === false ? 0 : new Date().getTime()
    timeout = null
    func.apply(context, args)
    if (!timeout) context = args = null
  }

  var throttled = function() {
    var now = new Date().getTime()
    if (!previous && options.leading === false) previous = now
    var remaining = delay - (now - previous)
    context = this
    args = arguments
    if (remaining <= 0 || remaining > delay) {
      if (timeout) {
        clearTimeout(timeout)
        timeout = null
      }
      previous = now
      func.apply(context, args)
      if (!timeout) context = args = null
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining)
    }
  }
  return throttled
}

我們想要第一次立即執(zhí)行,并且禁用停止觸發(fā)的回調(diào)琉历,調(diào)用throttle方法如下:

container.onmousemove = throttle(mouseMove, 2000, {leading: true, trailing: false})

效果如下圖:


java-throttle-4.gif

取消

在防抖函數(shù)中,我們實(shí)現(xiàn)了cancel方法水醋,在節(jié)流函數(shù)中旗笔,同理:

function throttle(func, delay, options) {
  .....

  var throttled = function() {
    ....
  }

  throttled.cancel = function() {
    clearTimeout(timeout)
    previous = 0
    timeout = null
  }

  return throttled
}

存在的問題

至此,一個(gè)完整的節(jié)流函數(shù)已經(jīng)實(shí)現(xiàn)好了拄踪,但是仍然存在一個(gè)問題:就是 leading:false 和 trailing: false 不能同時(shí)設(shè)置蝇恶。

如果同時(shí)設(shè)置的話,比如當(dāng)你將鼠標(biāo)移出的時(shí)候惶桐,因?yàn)?trailing 設(shè)置為 false撮弧,停止觸發(fā)的時(shí)候不會設(shè)置定時(shí)器,所以只要再過了設(shè)置的時(shí)間姚糊,再移入的話贿衍,就會立刻執(zhí)行,就違反了 leading: false救恨,bug 就出來了贸辈。如下圖所示:


java-throttle-5.gif

總結(jié)

防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用。區(qū)別在于:假設(shè)一個(gè)用戶一直觸發(fā)這個(gè)函數(shù)肠槽,且每次觸發(fā)函數(shù)的間隔小于wait擎淤,防抖的情況下只會調(diào)用一次,而節(jié)流的情況會每隔一段時(shí)間wait調(diào)用函數(shù)秸仙。

相比 debounce嘴拢,throttle 要更加寬松一些,其目的在于:按頻率執(zhí)行調(diào)用寂纪。

參考來源:https://github.com/mqyqingfeng/Blog/issues/26


歡迎關(guān)注微信公眾號:前端極客技術(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末席吴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子弊攘,更是在濱河造成了極大的恐慌抢腐,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件襟交,死亡現(xiàn)場離奇詭異迈倍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捣域,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門啼染,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宴合,“玉大人,你說我怎么就攤上這事迹鹅∝郧ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵斜棚,是天一觀的道長阀蒂。 經(jīng)常有香客問我,道長弟蚀,這世上最難降的妖魔是什么蚤霞? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮义钉,結(jié)果婚禮上昧绣,老公的妹妹穿的比我還像新娘。我一直安慰自己捶闸,他們只是感情好夜畴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著删壮,像睡著了一般贪绘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上醉锅,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天兔簇,我揣著相機(jī)與錄音,去河邊找鬼硬耍。 笑死垄琐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的经柴。 我是一名探鬼主播狸窘,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坯认!你這毒婦竟也來了翻擒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤牛哺,失蹤者是張志新(化名)和其女友劉穎陋气,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體引润,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巩趁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淳附。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片议慰。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蠢古,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出别凹,到底是詐尸還是另有隱情草讶,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布炉菲,位于F島的核電站蕴侧,受9級特大地震影響墓赴,放射性物質(zhì)發(fā)生泄漏芭毙。R本人自食惡果不足惜赚哗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沉御。 院中可真熱鬧,春花似錦昭灵、人聲如沸吠裆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽试疙。三九已至,卻和暖如春抠蚣,著一層夾襖步出監(jiān)牢的瞬間祝旷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工嘶窄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怀跛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓柄冲,卻偏偏與公主長得像吻谋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子现横,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348