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