前端開發中一個老生常談的問題就是'當用戶滾動時, 根據滾動的位置適當觸發不一樣的函數/動畫, 例如當元素出如今視口時觸發該元素的style改變. 一般的作法就是在scrollElement
上附加scoll
事件. 可是咱們知道, 當滾動條滾動時scroll
事件觸發的是很頻繁的, 且不禁JS控制(瀏覽器的事件隊列原生提供), 如圖(添加事件監聽後滾輪三格):javascript
依據系統設置的不一樣, 一次滾輪觸發的scroll
事件大概在10~15次之間. 若是在回調函數中添加大量的DOM操做或者計算的話, 會引發明顯的卡頓等性能問題. 那有沒有辦法去稀釋
回調函數的觸發操做呢? 這個時候就須要函數節流(throttle)和debounce(去顫抖)來解決了!html
這個版本運用閉包封裝數據, 修正this
指向以增強魯棒性, 剔除了一開始就顯示在視口的元素
talk is cheap, here are the code前端
// 根據單一元素, throttle函數專門負責事件稀釋, 接受兩個參數: 要間隔調用的函數, 以及調用間隔. var throttle = function (fn, interval) { let start = Date.now() let first = true return function (...args) { let now = Date.now() // 若是是第一次調用, 則忽略時間間隔 if (first) { fn.apply(this, args) first = false return } if (now - start > interval) { fn.apply(this, args) start = now } } } // 顯示元素的IIFE var showElems = (function (selector, func) { // 預處理, 標識已經顯示在視口的元素 let elemCollect = [...document.querySelectorAll(selector)] let innerHeight = window.innerHeight let hiddenElems = [] elemCollect.forEach((elem, index) => { let top = elem.getBoundingClientRect().top // 不顯示在視口才加入判斷隊列 if (top > innerHeight) { hiddenElems.push(elem) } }) // memory release elemCollect = null return function (...args) { hiddenElems.forEach((elem) => { let bottom = elem.getBoundingClientRect().bottom if (bottom < innerHeight) { func.apply(elem, args) } }) } })('p', function(e){ console.log(this, e, 'showed!') }) // 組合, throttle函數負責稀釋showElems觸發的頻率, showElems負責元素滾動到視口時的相應動做 var throttledScroll = throttle(showElems, 500) window.addEventListener('scroll', throttledScroll)
debounce
設想一些用戶的頻繁操做, 例如滾動, 文本框輸入等, 每次觸發事件都要調用回調函數; 這樣作的代價未免大了點. 而debounce
的做用就是在在用戶高頻觸發事件時, 暫時不調用回調, 在用戶中止高頻操做(例如中止輸入, 中止滾動時), 再調用一次回調. java
解決方案有了, 怎樣用代碼實現呢? 這裏咱們要用到setTimeout
這個功能來作函數調用的延遲. 具體代碼以下(將代碼粘貼到console中執行如下, 本身試試看):git
var timer; document.addEventListener('scroll', function(){ clearTimeout(timer); //若是操做時已經有了延遲執行, 則取消該延遲執行 timer = setTimeout(function() { //設定新的延遲執行 callback(); }, 500) })
(這裏咱們爲了方便說明, 設定了timer
全局變量. 實踐中咱們能夠將timer
附加爲函數的屬性, 隱藏在閉包中, 或者做爲對象的屬性等. )github
當第一次高頻操做觸發時, 設定一個timer
, 在500ms後執行; 若是用戶在500ms以內沒有再次進行該操做(本例中是滾動), 那麼咱們調用callback
; 然而若是500ms以內用戶又觸發了滾動(即所謂的高頻操做), 那麼咱們清除上一次設定的timeout
, 設定一個新的, 500ms以後執行的timeout
. 瀏覽器
你們思考一下, debounce
的本質就是在用戶觸發expensive操做時, 不斷延期該expensive操做的執行時間(取消和設定timeout
的代價是很小的). 當用戶中止操做, 那咱們就再也不延期, 最後一次設定的timeout
會在500ms後執行expensive operation, 例如dom操做, 計算等. 閉包
到這裏咱們彷佛已經有了一個解決方案! 然而還有個小小的問題.....app
若是用戶不停地操做, 那debounce
就會不斷把操做延期, 若是用戶沒有兩次操做的間隔時間大於500ms, 那麼咱們的callback
永遠也得不到執行. 可憐的callback
! 恩, 在這一點上咱們固然能夠改進...dom
throttle
throttle
的做用是, 保證一個函數至少間隔一段時間獲得一次執行. 不像等待用戶中止的debounce
, throttle
即便在用戶不停操做時, 也能讓callback
在操做期間獲得間隔的執行.
那麼該怎麼作呢? 一種方法是在用戶開始操做時記錄開始時間
, 同時設定一個flag ifOperationBegin = true
. 以後在每次用戶的操做中判斷當前時間
, 若是當前時間-開始時間 > 某個值
, 好比500ms, 則執行callback
, 同時設定ifOperationBegin = true
, 以開始下一次的設定開始時間 -> 記錄操做時間 -> 判斷的循環. 具體到代碼實現上:
var scrollBegin = false, scrollStartTime = null; //用戶還沒有開始操做 document.addEventListener('scroll', function(){ if(!scrollBegin)scrollStartTime = Date.now();//記錄開始時間, 前提是callback尚未被觸發過 scrollBegin = true;//設定flag if(Date.now() - scrollStartTime > 500){ //若是操做時間和開始時間間隔大於500ms則 exec(elems, cb); //調用回調 scrollBegin = false; //flag設爲false, 以設定新的開始時間 } })
這樣作的效果是, 在用戶持續觸發scroll
操做時, 保證在用戶操做期間callback
至少會每隔500ms觸發一次. 若是用戶操做在500ms以內結束, 那也木有關係, 下一次用戶從新開始操做時咱們的scrollStartTime
依然保留着, callback
會被當即觸發.
那這兩種技術能夠運用到哪裏呢? 請看以下代碼栗子:
function detectVisible(selector, cb, interval){ //檢測元素是否在視口的函數 var elems = document.querySelectorAll(selector), innerHeight = window.innerHeight; var exec = function(elems, cb){ //回調函數 Array.prototype.forEach.call(elems, function(elem, index){ if(elem.getBoundingClientRect().top < innerHeight){ //判斷元素是否出如今視口 cb.call(elem, elem); //調用傳入的回調 } }) } document.addEventListener('scroll', function(){ //使用debounce和throttle來稀釋scroll事件 clearTimeout(detectVisible.timer); if(!detectVisible.scrollBegin)detectVisible.scrollStartTime = Date.now(); detectVisible.scrollBegin = true; if(Date.now() - detectVisible.scrollStartTime > interval){ exec(elems, cb); console.log('invoked by throttle!') detectVisible.scrollBegin = false; } detectVisible.timer = setTimeout(function() { exec(elems, cb); console.log('invoked by debounce!') }, interval) }) } detectVisible('div.elem', function(elem){ this.style.backgroundColor = 'yellow'; }, 500);
這個栗子中咱們綜合運用了throttle
和debounce
, 達到了以下效果: 用戶不停滾動時callback
會至少每500ms觸發一次; 用戶中止滾動後的500ms判斷函數也會觸發一次. 你們能夠打開console查看callback
什麼時候是被throttle
觸發的, 什麼時候是被debounce
觸發的.
這一篇文章的主要主題事件的稀釋以期性能上的改善, 有兩種解決方法:throttle
和debounce
. 前者是經過在用戶操做時判斷操做時間, 來達到間隔一段時間觸發回調的效果; 然後者則是將觸發的時間不斷延期, 直到用戶中止操做再執行回調. 二者各有優缺點, 兩相結合, 咱們獲得了一個用戶不管怎樣操做(不停操做或者操做時間極短)均可以保證callback
按期獲得執行的函數. problem solved!
看完這篇, 若是你有所收穫, 請去github給我加個star唄!~