高階函數指的是至少知足下列兩個條件之一的函數:前端
1. 函數能夠做爲參數被傳遞;
2.函數能夠做爲返回值輸出;java
javaScript中的函數顯然具有高級函數的特徵,這使得函數運用更靈活,做爲學習js一定會接觸到的閉包也能夠經過高階函數構建,固然本文不打算介紹閉包,咱們今天的主題是函數防抖和節流。閉包
首先咱們來簡單看一下什麼是函數防抖和節流,咱們開發過程當中常常常會用到一些dom事件,好比mouseover、keydown/keyup、input(處理中文輸入還可能用到compositionstart/compositionend)、scroll和resize之類的事件,固然這些事件應用場景也很常見:app
1.如實現一些拖拽功能(不用 H5 Drag&Drop API)和鼠標移動須要觸發一些特殊需求的js操做會用mouseover;dom
2.處理輸入時候可能不少校驗,聯想之類的可能會用到keydown/keyup、input函數
3.一些射擊類遊戲的點擊可能會用到keydown/keyup學習
4.resize和scroll這兩個不用我多說了,滾動和窗口大小監聽測試
以上這些事件都有一個共同特色就是用戶的操做會觸發N屢次事件,好比最典型的scroll事件,滾動頁面會屢次重複觸發,致使不少咱們寫的回調函數被不停的往js的事件隊列裏添加,重複執行不少次回調裏的邏輯(若是你的回調裏還有不少操做DOM的邏輯那麼頁面可能會卡頓)。這些並非咱們但願的,因此咱們得采起必定的措施控制這種狀況:this
1.減小事件觸發的次數spa
2.減小回調的執行次數
顯然第一條減小事件觸發做爲寫前端的咱們應該是作不到的,因此解決方案就是減小回調的執行,由於咱們能控制是否往事件循環隊列裏添加回調函數和以什麼頻率去添加,
故而咱們要作的事讓回調函數不要執行得太頻繁,減小一些過快的調用以固定頻率(這個頻率咱們能夠控制)觸發來節流,或者讓短期內重複觸發的事件回調在事件中止觸發的必定時間後(這個時間咱們能夠控制)只執行一次回調函數。
好了,簡單的介紹了節流和防抖以後咱們開始來寫代碼演示一下,首先咱們先介紹函數防抖,由於防抖比節流更容易理解一些。
咱們如今打開控制檯給當前頁面綁定一個scroll事件:
而後吧頁面滾到底發現控制檯打印了不少個"test",若是咱們只但願頁面滾到底才觸發一些操做,那麼咱們只須要在最後只執行一次回調,這就須要防抖函數來實現,咱們這裏來一個簡化版本,其實這就是防抖的核心代碼,只要理解這點其餘的是複雜實現不過都是考慮更多的場景而已(《js高程》中說是節流,實則爲防抖)
function debounce(method, context, time) { clearTimeout(method.timeoutId); method.timeoutId = setTimeout(() => { method.apply(context,arguments); }, time); }
咱們再來試驗一下:
function test() { console.log('test') } window.addEventListener('scroll', function() { debounce(test, null, 200); })
這裏只要滾動事件的觸發頻率大於200ms就一直不會被執行,只有最後執行一次,若是你在寫的頁面測試,可能會在一開始執行了一次test由於刷新頁面觸發了scroll。
這裏給出underscore裏的防抖函數的實現(加粗地方對應上邊介紹的核心代碼邏輯):
// 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; };
接下來咱們說一說節流函數,在開始這以前咱們先來個簡單的代碼:
console.time('test throttle'); for (let i = 0; i < 5000; i++) { test(); } console.timeEnd('test throttle');
執行代碼後我這裏基本是在1.9s內打印了5000個「test」字符串,就是在很短期內調用了5000次,這5000次調用咱們但願每隔10ms的函數才被執行,
這裏咱們先來實現一個簡單版本的節流函數:
var throttle = (function() { var last = 0; return function(method, context, time) { var current = +new Date(); if (current - last > time) { method.apply(context, arguments); last = current; } } })();
再次執行咱們能夠獲得:
調用共用了138ms(時間比前邊少不少,這是由於咱們前邊實際都執行了打印的操做),而這裏throttle函數知識把test()函數放進js函數執行隊列,並無執行打印,因此執行時間要少不少,不過咱們仍是獲得了咱們指望的效果:函數在屢次調用的時候咱們指望固定頻率執行函數,而忽略其餘函數不執行。
這裏咱們一樣給出underscore裏的節流函數版本:
// 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; }; 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; };