在前端開發中,有一部分用戶行爲會頻繁的觸發事件,而對於DOM操做,資源加載等耗費性能的處理,極可能會致使卡頓,甚至瀏覽器的崩潰。防抖和節流就是爲了解決這一類的問題。javascript
window.onscroll = function () { //滾動條位置 let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滾動條位置:' + scrollTop); }
從效果上,咱們能夠看到,在頁面滾動的時候,會在短期內觸發屢次綁定事件。html
咱們知道DOM操做是很耗費性能的,若是在監聽中,作了一些DOM操做,那無疑會給瀏覽器形成大量性能損失。前端
下面咱們進入主題,一塊兒來探究,如何對此進行優化。java
理解:在車站上車,人員上滿了車才發走重點是人員上滿觸發一次。瀏覽器
緣由緩存
定義:屢次觸發事件後,事件處理函數只執行一次,而且是在觸發操做結束時執行。閉包
原理:對處理函數進行延時操做,若設定的延時到來以前,再次觸發事件,則清除上一次的延時操做定時器,從新定時。app
參考連接函數
// 計時器 var timer = false; // window.onscroll = function(){ clearTimeout(timer); timer = setTimeout(function(){ console.log("防抖"); console.log(new Date()); },300); };
爲何要clearTimeoutoop
每次onscroll的時候,先清除掉計時器.若是不清楚,會致使屢次觸發的時候,實際上是把好屢次的處理方法放在某個時間點後一塊兒執行。
好比下面: for (var i = 0; i < 10; i++) { (function (i) { setTimeout(function () { console.log(i); }, 3000); })(i); }
上面代碼在3秒後會一塊兒輸出 1,2,3,4,5,6,7,8,9
而下面的代碼,只會輸出9
var timer2 = false; for (var i = 0; i < 10; i++) { clearTimeout(timer2); (function (i) { timer2 = setTimeout(function () { console.log(i); }, 3000); })(i); }
這是由於,每次我將上次的timer給清除掉了,也就是我若是後面一樣有處理函數的話,那我就用後面的定時器。
前面定時器沒啥用了,因此直接clearTimeout保證了這種實現。
在解決onscroll問題的時候,若是本身觀察console能夠發現,防抖保證了滾動中止的時候,纔會進行處理,由於滾動中止了,沒有scroll事件了,最後一次timer會被保留,從而進行調用
//每一次都要清空定時器,從新設置上計時器值,使得計時器每一次都從新開始,直到最後知足條件而且等待delay時間後,纔開始執行handler函數。 let timer ; window.onscroll = function(){ if(timer){ clearTimeout(timer); } timer = setTimeout(function(){ //滾動條位置 let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滾動條位置'+ scrollTop); timer = null; },2000) console.log(timer);//一滾動,timer就有對應的數值,即 //setTimeout會返回數值,clearTimeout(這個數值),就至關於清除這個定時器
效果以下:滾動結束觸發事件
// func是用戶傳入須要防抖的函數 // wait是等待時間 const debounce = (func, wait = 50) => { // 緩存一個定時器id let timer = 0 // 這裏返回的函數是每次用戶實際調用的防抖函數 // 若是已經設定過定時器了就清空上一次的定時器 // 開始一個新的定時器,延遲執行用戶傳入的方法 return function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { func.apply(this, args) }, wait) } } window.onscroll = debounce(function(){ let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滾動條位置:' + scrollTop); } ,200) // 不難看出若是用戶調用該函數的間隔小於wait的狀況下,上一次的時間還未到就被清除了,並不會執行函數
這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:
例如在搜索引擎搜索問題的時候,咱們固然是但願用戶輸入完最後一個字才調用查詢接口,這個時候適用延遲執行的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。
例如用戶給interviewMap點star的時候,咱們但願用戶點第一下的時候就去調用接口,而且成功以後改變star按鈕的樣子,用戶就能夠立馬獲得反饋是否star成功了,這個狀況適用當即執行的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。
// 這個是用來獲取當前時間戳的 function now() { return +new Date() } /** * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行 * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {boolean} immediate 設置爲ture時,是否當即調用函數 * @return {function} 返回客戶調用函數 */ function debounce (func, wait = 50, immediate = true) { let timer, context, args // 延遲執行函數 const later = () => setTimeout(() => { // 延遲函數執行完畢,清空緩存的定時器序號 timer = null // 延遲執行的狀況下,函數會在延遲函數中執行 // 使用到以前緩存的參數和上下文 if (!immediate) { func.apply(context, args) context = args = null } }, wait) // 這裏返回的函數是每次實際調用的函數 return function(...params) { // 若是沒有建立延遲執行函數(later),就建立一個 if (!timer) { timer = later() // 若是是當即執行,調用函數 // 不然緩存參數和調用上下文 if (immediate) { func.apply(this, params) } else { context = this args = params } // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個 // 這樣作延遲函數會從新計時 } else { clearTimeout(timer) timer = later() } } }
對於==按鈕防點擊來==說的實現:若是函數是當即執行的,就當即調用,若是函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都從新計時。一旦你點累了,定時器時間到,定時器重置爲 null,就能夠再次點擊了。
對於延時執行函數來講的實現:清除定時器ID,若是是延遲調用就調用函數
經過上面的例子,咱們知道咱們能夠經過函數防抖,解決了屢次觸發事件時的性能問題。
好比,咱們在監聽滾動條位置,控制是否顯示返回頂部按鈕時,就能夠將防抖函數應用其中。
當咱們作圖片懶加載(lazyload)時,須要經過滾動位置,實時顯示圖片時,若是使用防抖函數,懶加載(lazyload)函數將會不斷被延時,
只有停下來的時候纔會被執行,對於這種須要實時觸發事件的狀況,就顯得不是很友好了。
下面開始介紹函數節流,經過設定時間片,控制事件函數間斷性的觸發。
理解:大於等於10分鐘發一次車,重點是必定間隔時間就會觸發一次。
定義:觸發函數事件後,短期間隔內沒法連續調用,只有上一次函數執行後,過了規定的時間間隔,才能進行下一次的函數調用。
原理:對處理函數進行延時操做,若設定的延時到來以前,再次觸發事件,則清除上一次的延時操做定時器,從新定時。
簡單來講,函數的節流就是經過閉包保存一個標記(canRun = true),在函數的開頭判斷這個標記是否爲true,若是爲true的話就繼續執行函數,不然則 return 掉,判斷完標記後當即把這個標記設爲false,而後把外部傳入的函數的執行包在一個setTimeout中,最後在setTimeout執行完畢後再把標記設置爲true(這裏很關鍵),表示能夠執行下一次的循環了。
當setTimeout還未執行的時候,canRun這個標記始終爲false,在開頭的判斷中被 return 掉。
function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); }; }
scroll 的一個簡單例子
let startTime = Date.now(); //開始時間 let time = 500; //間隔時間 let timer; window.onscroll = function throttle(){ let currentTime = Date.now(); if(currentTime - startTime >= time){ let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滾動條位置:' + scrollTop); startTime = currentTime; }else{ clearTimeout(timer); timer = setTimeout(function () { throttle() }, 50); } }
- 效果以下:每隔500毫秒觸發一次事件
/** * 節流函數 * @param method 事件觸發的操做 * @param mustRunDelay 間隔多少毫秒須要觸發一次事件 */ function throttle(method, mustRunDelay) { let timer, args = arguments, start; return function loop() { let self = this; let now = Date.now(); if(!start){ start = now; } if(timer){ clearTimeout(timer); } if(now - start >= mustRunDelay){ method.apply(self, args); start = now; }else { timer = setTimeout(function () { loop.apply(self, args); }, 50); } } } window.onscroll = throttle(function () { let scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log('滾動條位置:' + scrollTop); },800)
上面的代碼一刷新就觸發事件,即打印出'滾動條位置"
** * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait * * @param {function} func 回調函數 * @param {number} wait 表示時間窗口的間隔 * @param {object} options 若是想忽略開始函數的的調用,傳入{leading: false}。 * 若是想忽略結尾函數的調用,傳入{trailing: false} * 二者不能共存,不然函數不能執行 * @return {function} 返回客戶調用函數 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 以前的時間戳 var previous = 0; // 若是 options 沒傳則設爲空對象 if (!options) options = {}; // 定時器回調函數 var later = function() { // 若是設置了 leading,就將 previous 設爲 0 // 用於下面函數的第一個 if 判斷 previous = options.leading === false ? 0 : _.now(); // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 得到當前時間戳 var now = _.now(); // 首次進入前者確定爲 true // 若是須要第一次不執行函數 // 就將上次時間戳設爲當前的 // 這樣在接下來計算 remaining 的值時會大於0 if (!previous && options.leading === false) previous = now; // 計算剩餘時間 var remaining = wait - (now - previous); context = this; args = arguments; // 若是當前調用已經大於上次調用時間 + wait // 或者用戶手動調了時間 // 若是設置了 trailing,只會進入這個條件 // 若是沒有設置 leading,那麼第一次會進入這個條件 // 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了 // 其實仍是會進入的,由於定時器的延時 // 並非準確的時間,極可能你設置了2秒 // 可是他須要2.2秒才觸發,這時候就會進入這個條件 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) { // 判斷是否設置了定時器和 trailing // 沒有的話就開啓一個定時器 // 而且不能不能同時設置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };