摘要:引言上一節(jié)我們?cè)敿?xì)聊了聊高階函數(shù)之柯里化,通過(guò)介紹其定義和三種柯里化應(yīng)用,并在最后實(shí)現(xiàn)了一個(gè)通用的函數(shù)。第二種方案來(lái)實(shí)現(xiàn)也存在一個(gè)問(wèn)題,因?yàn)槎〞r(shí)器是延遲執(zhí)行的,所以事件停止觸發(fā)時(shí)必然會(huì)響應(yīng)回調(diào),所以時(shí)無(wú)法生效。
引言
上一節(jié)我們?cè)敿?xì)聊了聊高階函數(shù)之柯里化,通過(guò)介紹其定義和三種柯里化應(yīng)用,并在最后實(shí)現(xiàn)了一個(gè)通用的 currying 函數(shù)。這一小節(jié)會(huì)繼續(xù)之前的篇幅聊聊函數(shù)節(jié)流 throttle,給出這種高階函數(shù)的定義、實(shí)現(xiàn)原理以及在 underscore 中的實(shí)現(xiàn),歡迎大家拍磚。
有什么想法或者意見(jiàn)都可以在評(píng)論區(qū)留言,下圖是本文的思維導(dǎo)圖,高清思維導(dǎo)圖和更多文章請(qǐng)看我的 Github。
定義及解讀函數(shù)節(jié)流指的是某個(gè)函數(shù)在一定時(shí)間間隔內(nèi)(例如 3 秒)只執(zhí)行一次,在這 3 秒內(nèi) 無(wú)視后來(lái)產(chǎn)生的函數(shù)調(diào)用請(qǐng)求,也不會(huì)延長(zhǎng)時(shí)間間隔。3 秒間隔結(jié)束后第一次遇到新的函數(shù)調(diào)用會(huì)觸發(fā)執(zhí)行,然后在這新的 3 秒內(nèi)依舊無(wú)視后來(lái)產(chǎn)生的函數(shù)調(diào)用請(qǐng)求,以此類(lèi)推。
舉一個(gè)小例子,不知道大家小時(shí)候有沒(méi)有養(yǎng)過(guò)小金魚(yú)啥的,養(yǎng)金魚(yú)肯定少不了接水,剛開(kāi)始接水時(shí)管道中水流很大,水到半滿(mǎn)時(shí)開(kāi)始擰緊水龍頭,減少水流的速度變成 3 秒一滴,通過(guò)滴水給小金魚(yú)增加氧氣。
此時(shí)「管道中的水」就是我們頻繁操作事件而不斷涌入的回調(diào)任務(wù),它需要接受「水龍頭」安排;「水龍頭」就是節(jié)流閥,控制水的流速,過(guò)濾無(wú)效的回調(diào)任務(wù);「滴水」就是每隔一段時(shí)間執(zhí)行一次函數(shù),「3 秒」就是間隔時(shí)間,它是「水龍頭」決定「滴水」的依據(jù)。
如果你還無(wú)法理解,看下面這張圖就清晰多了,另外點(diǎn)擊 這個(gè)頁(yè)面 查看節(jié)流和防抖的可視化比較。其中 Regular 是不做任何處理的情況,throttle 是函數(shù)節(jié)流之后的結(jié)果,debounce 是函數(shù)防抖之后的結(jié)果(下一小節(jié)介紹)。
原理及實(shí)現(xiàn)函數(shù)節(jié)流非常適用于函數(shù)被頻繁調(diào)用的場(chǎng)景,例如:window.onresize() 事件、mousemove 事件、上傳進(jìn)度等情況。使用 throttle API 很簡(jiǎn)單,那應(yīng)該如何實(shí)現(xiàn) throttle 這個(gè)函數(shù)呢?
實(shí)現(xiàn)方案有以下兩種
第一種是用時(shí)間戳來(lái)判斷是否已到執(zhí)行時(shí)間,記錄上次執(zhí)行的時(shí)間戳,然后每次觸發(fā)事件執(zhí)行回調(diào),回調(diào)中判斷當(dāng)前時(shí)間戳距離上次執(zhí)行時(shí)間戳的間隔是否已經(jīng)達(dá)到時(shí)間差(Xms) ,如果是則執(zhí)行,并更新上次執(zhí)行的時(shí)間戳,如此循環(huán)。
第二種方法是使用定時(shí)器,比如當(dāng) scroll 事件剛觸發(fā)時(shí),打印一個(gè) hello world,然后設(shè)置個(gè) 1000ms 的定時(shí)器,此后每次觸發(fā) scroll 事件觸發(fā)回調(diào),如果已經(jīng)存在定時(shí)器,則回調(diào)不執(zhí)行方法,直到定時(shí)器觸發(fā),handler 被清除,然后重新設(shè)置定時(shí)器。
這里我們采用第一種方案來(lái)實(shí)現(xiàn),通過(guò)閉包保存一個(gè) previous 變量,每次觸發(fā) throttle 函數(shù)時(shí)判斷當(dāng)前時(shí)間和 previous 的時(shí)間差,如果這段時(shí)間差小于等待時(shí)間,那就忽略本次事件觸發(fā)。如果大于等待時(shí)間就把 previous 設(shè)置為當(dāng)前時(shí)間并執(zhí)行函數(shù) fn。
我們來(lái)一步步實(shí)現(xiàn),首先實(shí)現(xiàn)用閉包保存 previous 變量。
const throttle = (fn, wait) => { // 上一次執(zhí)行該函數(shù)的時(shí)間 let previous = 0 return function(...args) { console.log(previous) ... } }
執(zhí)行 throttle 函數(shù)后會(huì)返回一個(gè)新的 function,我們命名為 betterFn。
const betterFn = function(...args) { console.log(previous) ... }
betterFn 函數(shù)中可以獲取到 previous 變量值也可以修改,在回調(diào)監(jiān)聽(tīng)或事件觸發(fā)時(shí)就會(huì)執(zhí)行 betterFn,即 betterFn(),所以在這個(gè)新函數(shù)內(nèi)判斷當(dāng)前時(shí)間和 previous 的時(shí)間差即可。
const betterFn = function(...args) { let now = +new Date(); if (now - previous > wait) { previous = now // 執(zhí)行 fn 函數(shù) fn.apply(this, args) } }
結(jié)合上面兩段代碼就實(shí)現(xiàn)了節(jié)流函數(shù),所以完整的實(shí)現(xiàn)如下。
// fn 是需要執(zhí)行的函數(shù) // wait 是時(shí)間間隔 const throttle = (fn, wait = 50) => { // 上一次執(zhí)行 fn 的時(shí)間 let previous = 0 // 將 throttle 處理結(jié)果當(dāng)作函數(shù)返回 return function(...args) { // 獲取當(dāng)前時(shí)間,轉(zhuǎn)換成時(shí)間戳,單位毫秒 let now = +new Date() // 將當(dāng)前時(shí)間和上一次執(zhí)行函數(shù)的時(shí)間進(jìn)行對(duì)比 // 大于等待時(shí)間就把 previous 設(shè)置為當(dāng)前時(shí)間并執(zhí)行函數(shù) fn if (now - previous > wait) { previous = now fn.apply(this, args) } } } // DEMO // 執(zhí)行 throttle 函數(shù)返回新函數(shù) const betterFn = throttle(() => console.log("fn 函數(shù)執(zhí)行了"), 1000) // 每 10 秒執(zhí)行一次 betterFn 函數(shù),但是只有時(shí)間差大于 1000 時(shí)才會(huì)執(zhí)行 fn setInterval(betterFn, 10)underscore 源碼解讀
上述代碼實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的節(jié)流函數(shù),不過(guò) underscore 實(shí)現(xiàn)了更高級(jí)的功能,即新增了兩個(gè)功能
配置是否需要響應(yīng)事件剛開(kāi)始的那次回調(diào)( leading 參數(shù),false 時(shí)忽略)
配置是否需要響應(yīng)事件結(jié)束后的那次回調(diào)( trailing 參數(shù),false 時(shí)忽略)
配置 { leading: false } 時(shí),事件剛開(kāi)始的那次回調(diào)不執(zhí)行;配置 { trailing: false } 時(shí),事件結(jié)束后的那次回調(diào)不執(zhí)行,不過(guò)需要注意的是,這兩者不能同時(shí)配置。
所以在 underscore 中的節(jié)流函數(shù)有 3 種調(diào)用方式,默認(rèn)的(有頭有尾),設(shè)置 { leading: false } 的,以及設(shè)置 { trailing: false } 的。上面說(shuō)過(guò)實(shí)現(xiàn) throttle 的方案有 2 種,一種是通過(guò)時(shí)間戳判斷,另一種是通過(guò)定時(shí)器創(chuàng)建和銷(xiāo)毀來(lái)控制。
第一種方案實(shí)現(xiàn)這 3 種調(diào)用方式存在一個(gè)問(wèn)題,即事件停止觸發(fā)時(shí)無(wú)法響應(yīng)回調(diào),所以 { trailing: true } 時(shí)無(wú)法生效。
第二種方案來(lái)實(shí)現(xiàn)也存在一個(gè)問(wèn)題,因?yàn)槎〞r(shí)器是延遲執(zhí)行的,所以事件停止觸發(fā)時(shí)必然會(huì)響應(yīng)回調(diào),所以 { trailing: false } 時(shí)無(wú)法生效。
underscore 采用的方案是兩種方案搭配使用來(lái)實(shí)現(xiàn)這個(gè)功能。
const throttle = function(func, wait, options) { var timeout, context, args, result; // 上一次執(zhí)行回調(diào)的時(shí)間戳 var previous = 0; // 無(wú)傳入?yún)?shù)時(shí),初始化 options 為空對(duì)象 if (!options) options = {}; var later = function() { // 當(dāng)設(shè)置 { leading: false } 時(shí) // 每次觸發(fā)回調(diào)函數(shù)后設(shè)置 previous 為 0 // 不然為當(dāng)前時(shí)間 previous = options.leading === false ? 0 : _.now(); // 防止內(nèi)存泄漏,置為 null 便于后面根據(jù) !timeout 設(shè)置新的 timeout timeout = null; // 執(zhí)行函數(shù) result = func.apply(context, args); if (!timeout) context = args = null; }; // 每次觸發(fā)事件回調(diào)都執(zhí)行這個(gè)函數(shù) // 函數(shù)內(nèi)判斷是否執(zhí)行 func // func 才是我們業(yè)務(wù)層代碼想要執(zhí)行的函數(shù) var throttled = function() { // 記錄當(dāng)前時(shí)間 var now = _.now(); // 第一次執(zhí)行時(shí)(此時(shí) previous 為 0,之后為上一次時(shí)間戳) // 并且設(shè)置了 { leading: false }(表示第一次回調(diào)不執(zhí)行) // 此時(shí)設(shè)置 previous 為當(dāng)前值,表示剛執(zhí)行過(guò),本次就不執(zhí)行了 if (!previous && options.leading === false) previous = now; // 距離下次觸發(fā) func 還需要等待的時(shí)間 var remaining = wait - (now - previous); context = this; args = arguments; // 要么是到了間隔時(shí)間了,隨即觸發(fā)方法(remaining <= 0) // 要么是沒(méi)有傳入 {leading: false},且第一次觸發(fā)回調(diào),即立即觸發(fā) // 此時(shí) previous 為 0,wait - (now - previous) 也滿(mǎn)足 <= 0 // 之后便會(huì)把 previous 值迅速置為 now if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); // clearTimeout(timeout) 并不會(huì)把 timeout 設(shè)為 null // 手動(dòng)設(shè)置,便于后續(xù)判斷 timeout = null; } // 設(shè)置 previous 為當(dāng)前時(shí)間 previous = now; // 執(zhí)行 func 函數(shù) result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 最后一次需要觸發(fā)的情況 // 如果已經(jīng)存在一個(gè)定時(shí)器,則不會(huì)進(jìn)入該 if 分支 // 如果 {trailing: false},即最后一次不需要觸發(fā)了,也不會(huì)進(jìn)入這個(gè)分支 // 間隔 remaining milliseconds 后觸發(fā) later 方法 timeout = setTimeout(later, remaining); } return result; }; // 手動(dòng)取消 throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; // 執(zhí)行 _.throttle 返回 throttled 函數(shù) return throttled; };小結(jié)
函數(shù)節(jié)流指的是某個(gè)函數(shù)在一定時(shí)間間隔內(nèi)(例如 3 秒)只執(zhí)行一次,在這 3 秒內(nèi) 無(wú)視后來(lái)產(chǎn)生的函數(shù)調(diào)用請(qǐng)求
節(jié)流可以理解為養(yǎng)金魚(yú)時(shí)擰緊水龍頭放水,3 秒一滴
「管道中的水」就是我們頻繁操作事件而不斷涌入的回調(diào)任務(wù),它需要接受「水龍頭」安排
「水龍頭」就是節(jié)流閥,控制水的流速,過(guò)濾無(wú)效的回調(diào)任務(wù)
「滴水」就是每隔一段時(shí)間執(zhí)行一次函數(shù)
「3 秒」就是間隔時(shí)間,它是「水龍頭」決定「滴水」的依據(jù)
節(jié)流實(shí)現(xiàn)方案有 2 種
第一種是用時(shí)間戳來(lái)判斷是否已到執(zhí)行時(shí)間,記錄上次執(zhí)行的時(shí)間戳,然后每次觸發(fā)事件執(zhí)行回調(diào),回調(diào)中判斷當(dāng)前時(shí)間戳距離上次執(zhí)行時(shí)間戳的間隔是否已經(jīng)達(dá)到時(shí)間差(Xms) ,如果是則執(zhí)行,并更新上次執(zhí)行的時(shí)間戳,如此循環(huán)。
第二種方法是使用定時(shí)器,比如當(dāng) scroll 事件剛觸發(fā)時(shí),打印一個(gè) hello world,然后設(shè)置個(gè) 1000ms 的定時(shí)器,此后每次觸發(fā) scroll 事件觸發(fā)回調(diào),如果已經(jīng)存在定時(shí)器,則回調(diào)不執(zhí)行方法,直到定時(shí)器觸發(fā),handler 被清除,然后重新設(shè)置定時(shí)器。
參考underscore.js文章穿梭機(jī)前端性能優(yōu)化原理與實(shí)踐
underscore 函數(shù)節(jié)流的實(shí)現(xiàn)
【進(jìn)階 6-2 期】深入高階函數(shù)應(yīng)用之柯里化
【進(jìn)階 6-1 期】JavaScript 高階函數(shù)淺析
【進(jìn)階 5-3 期】深入探究 Function & Object 雞蛋問(wèn)題
【進(jìn)階 5-2 期】圖解原型鏈及其繼承優(yōu)缺點(diǎn)
【進(jìn)階 5-1 期】重新認(rèn)識(shí)構(gòu)造函數(shù)、原型和原型鏈
?? 看完三件事如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)贊,讓更多的人也能看到這篇內(nèi)容(收藏不點(diǎn)贊,都是耍流氓 -_-)
關(guān)注我的 GitHub,讓我們成為長(zhǎng)期關(guān)系
關(guān)注公眾號(hào)「高級(jí)前端進(jìn)階」,每周重點(diǎn)攻克一個(gè)前端面試重難點(diǎn),公眾號(hào)后臺(tái)回復(fù)「資料」 送你精選前端優(yōu)質(zhì)資料。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/109934.html
摘要:主要實(shí)現(xiàn)思路就是通過(guò)定時(shí)器,通過(guò)設(shè)置延時(shí)時(shí)間,在第一次調(diào)用時(shí),創(chuàng)建定時(shí)器,寫(xiě)入需要執(zhí)行的函數(shù)。如果這時(shí)前一個(gè)定時(shí)器暫未執(zhí)行,則將其替換為新的定時(shí)器。 JS中的函數(shù)節(jié)流 一、什么是函數(shù)節(jié)流(throttle) 概念:限制一個(gè)函數(shù)在一定時(shí)間內(nèi)只能執(zhí)行一次。 舉個(gè)栗子,坐火車(chē)或地鐵,過(guò)安檢的時(shí)候,在一定時(shí)間(例如10秒)內(nèi),只允許一個(gè)乘客通過(guò)安檢入口,以配合安檢人員完成安檢工作。上例中,每1...
摘要:函數(shù)節(jié)流的原理函數(shù)節(jié)流的原理挺簡(jiǎn)單的,估計(jì)大家都想到了,那就是定時(shí)器。在高級(jí)程序設(shè)計(jì)一書(shū)有介紹函數(shù)節(jié)流,里面封裝了這樣一個(gè)函數(shù)節(jié)流函數(shù),它把定時(shí)器存為函數(shù)的一個(gè)屬性個(gè)人的世界觀不喜歡這種寫(xiě)法。 什么是函數(shù)節(jié)流? 介紹前,先說(shuō)下背景。在前端開(kāi)發(fā)中,有時(shí)會(huì)為頁(yè)面綁定resize事件,或者為一個(gè)頁(yè)面元素綁定拖拽事件(其核心就是綁定mousemove),這種事件有一個(gè)特點(diǎn),就是用戶(hù)不必特地?fù)v亂...
摘要:使用上一篇文章的例子來(lái)說(shuō)明下自由變量進(jìn)階期深入淺出圖解作用域鏈和閉包訪問(wèn)外部的今天是今天是其中既不是參數(shù),也不是局部變量,所以是自由變量。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開(kāi)始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第7天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問(wèn)外鏈,點(diǎn)擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對(duì)象,在全局環(huán)境中定義的變量就會(huì)綁定到全局對(duì)象中。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周開(kāi)始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:本計(jì)劃一共期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)階計(jì)劃,點(diǎn)擊查看前端進(jìn)階的破冰之旅本期推薦文章深入之執(zhí)行上下文棧和深入之變量對(duì)象,由于微信不能訪問(wèn)外鏈,點(diǎn)擊閱讀原文就可以啦。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開(kāi)始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,今天是第二天。 本計(jì)劃一共28期,每期...
閱讀 3080·2021-10-12 10:12
閱讀 5475·2021-09-26 10:20
閱讀 1540·2021-07-26 23:38
閱讀 2834·2019-08-30 15:54
閱讀 1665·2019-08-30 13:45
閱讀 1984·2019-08-30 11:23
閱讀 3118·2019-08-29 13:49
閱讀 904·2019-08-26 18:23