摘要:引言前端開發中一個老生常談的問題就是當用戶滾動時根據滾動的位置適當觸發不同的函數動畫例如當元素出現在視口時觸發該元素的改變通常的做法就是在上附加事件但是我們知道當滾動條滾動時事件觸發的是很頻繁的且不由控制瀏覽器的事件隊列原生提供如圖添加事件
引言
前端開發中一個老生常談的問題就是"當用戶滾動時, 根據滾動的位置適當觸發不同的函數/動畫, 例如當元素出現在視口時觸發該元素的style改變. 通常的做法就是在scrollElement上附加scoll事件. 但是我們知道, 當滾動條滾動時scroll事件觸發的是很頻繁的, 且不由JS控制(瀏覽器的事件隊列原生提供), 如圖(添加事件監聽后滾輪三格):
依據系統設置的不同, 一次滾輪觸發的scroll事件大概在10~15次之間. 如果在回調函數中添加大量的DOM操作或者計算的話, 會引起明顯的卡頓等性能問題. 那有沒有辦法去稀釋回調函數的觸發操作呢? 這個時候就需要函數節流(throttle)和debounce(去顫抖)來解決了!
2017-02-06更新函數式版本這個版本運用閉包封裝數據, 修正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的作用就是在在用戶高頻觸發事件時, 暫時不調用回調, 在用戶停止高頻操作(例如停止輸入, 停止滾動時), 再調用一次回調.
解決方案有了, 怎樣用代碼實現呢? 這里我們要用到setTimeout這個功能來做函數調用的延遲. 具體代碼如下(將代碼粘貼到console中執行以下, 自己試試看):
var timer; document.addEventListener("scroll", function(){ clearTimeout(timer); //如果操作時已經有了延遲執行, 則取消該延遲執行 timer = setTimeout(function() { //設定新的延遲執行 callback(); }, 500) })
(這里我們為了方便說明, 設定了timer全局變量. 實踐中我們可以將timer附加為函數的屬性, 隱藏在閉包中, 或者作為對象的屬性等. )
當第一次高頻操作觸發時, 設定一個timer, 在500ms后執行; 如果用戶在500ms之內沒有再次進行該操作(本例中是滾動), 那么我們調用callback; 然而如果500ms之內用戶又觸發了滾動(即所謂的高頻操作), 那么我們清除上一次設定的timeout, 設定一個新的, 500ms之后執行的timeout.
大家思考一下, debounce的本質就是在用戶觸發expensive操作時, 不斷延期該expensive操作的執行時間(取消和設定timeout的代價是很小的). 當用戶停止操作, 那我們就不再延期, 最后一次設定的timeout會在500ms后執行expensive operation, 例如dom操作, 計算等.
到這里我們似乎已經有了一個解決方案! 然而還有個小小的問題.....
如果用戶不停地操作, 那debounce就會不斷把操作延期, 如果用戶沒有兩次操作的間隔時間大于500ms, 那么我們的callback永遠也得不到執行. 可憐的callback! 恩, 在這一點上我們當然可以改進...
throttlethrottle的作用是, 保證一個函數至少間隔一段時間得到一次執行. 不像等待用戶停止的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唄!~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88025.html
摘要:引言前端開發中一個老生常談的問題就是當用戶滾動時根據滾動的位置適當觸發不同的函數動畫例如當元素出現在視口時觸發該元素的改變通常的做法就是在上附加事件但是我們知道當滾動條滾動時事件觸發的是很頻繁的且不由控制瀏覽器的事件隊列原生提供如圖添加事件 引言 前端開發中一個老生常談的問題就是當用戶滾動時, 根據滾動的位置適當觸發不同的函數/動畫, 例如當元素出現在視口時觸發該元素的style改變....
摘要:配置項配置項中的參數有以下三個所監聽對象的具體祖先元素,默認是計算交叉狀態時,將附加到祖先元素上,從而有效的擴大或者縮小祖先元素判定區域設置一系列的閾值,當交叉狀態達到閾值時,會觸發回調函數。 一、前言 ??通常情況下,HTML 中的圖片資源會自上而下依次加載,而部分圖片只有在用戶向下滾動頁面的場景下才能被看見,否則這部分圖片的流量就白白浪費了。 ??所以,對于那些含有大量圖片資源的網...
摘要:用于獲得當前元素到定位父級頂部的距離偏移值。后來在項目中總會遇到滾動吸頂的效果需要實現,現在我將我知道的種滾動吸頂實現方式做詳細介紹。有兼容性問題,在微信瀏覽器某些版本中的值會為,于是乎也就有了第三種方案的兼容性寫法。修改版預覽 這篇文章是三天前寫就的,有大佬給我提了一些修改意見,我覺得這個意見確實中肯。所以就有了這個升級的修改版本。代碼同步更新到 GitHub 了。 修改內容如下: 添加...
摘要:原文鏈接延遲加載也稱為惰性加載,即在長網頁中延遲加載圖像。傳入一個回調函數,當其觀察到元素集合出現時候,則會執行該函數。管理的是一個數組,當元素出現或消失的時候,數組添加或刪除該元素,并且執行該回調函數。 原文鏈接 - https://zhuanlan.zhihu.com/p/25455672 延遲加載也稱為惰性加載,即在長網頁中延遲加載圖像。用戶滾動到它們之前,視口外的圖像不會加載。...
閱讀 2178·2023-04-25 19:06
閱讀 1375·2021-11-17 09:33
閱讀 1767·2019-08-30 15:53
閱讀 2582·2019-08-30 14:20
閱讀 3541·2019-08-29 12:58
閱讀 3534·2019-08-26 13:27
閱讀 501·2019-08-26 12:23
閱讀 485·2019-08-26 12:22