摘要:引言前端開發(fā)中一個老生常談的問題就是當(dāng)用戶滾動時根據(jù)滾動的位置適當(dāng)觸發(fā)不同的函數(shù)動畫例如當(dāng)元素出現(xiàn)在視口時觸發(fā)該元素的改變通常的做法就是在上附加事件但是我們知道當(dāng)滾動條滾動時事件觸發(fā)的是很頻繁的且不由控制瀏覽器的事件隊(duì)列原生提供如圖添加事件
引言
前端開發(fā)中一個老生常談的問題就是"當(dāng)用戶滾動時, 根據(jù)滾動的位置適當(dāng)觸發(fā)不同的函數(shù)/動畫, 例如當(dāng)元素出現(xiàn)在視口時觸發(fā)該元素的style改變. 通常的做法就是在scrollElement上附加scoll事件. 但是我們知道, 當(dāng)滾動條滾動時scroll事件觸發(fā)的是很頻繁的, 且不由JS控制(瀏覽器的事件隊(duì)列原生提供), 如圖(添加事件監(jiān)聽后滾輪三格):
依據(jù)系統(tǒng)設(shè)置的不同, 一次滾輪觸發(fā)的scroll事件大概在10~15次之間. 如果在回調(diào)函數(shù)中添加大量的DOM操作或者計(jì)算的話, 會引起明顯的卡頓等性能問題. 那有沒有辦法去稀釋回調(diào)函數(shù)的觸發(fā)操作呢? 這個時候就需要函數(shù)節(jié)流(throttle)和debounce(去顫抖)來解決了!
2017-02-06更新函數(shù)式版本這個版本運(yùn)用閉包封裝數(shù)據(jù), 修正this指向以加強(qiáng)魯棒性, 剔除了一開始就顯示在視口的元素
talk is cheap, here are the code
// 根據(jù)單一元素, throttle函數(shù)專門負(fù)責(zé)事件稀釋, 接受兩個參數(shù): 要間隔調(diào)用的函數(shù), 以及調(diào)用間隔. var throttle = function (fn, interval) { let start = Date.now() let first = true return function (...args) { let now = Date.now() // 如果是第一次調(diào)用, 則忽略時間間隔 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) { // 預(yù)處理, 標(biāo)識已經(jīng)顯示在視口的元素 let elemCollect = [...document.querySelectorAll(selector)] let innerHeight = window.innerHeight let hiddenElems = [] elemCollect.forEach((elem, index) => { let top = elem.getBoundingClientRect().top // 不顯示在視口才加入判斷隊(duì)列 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函數(shù)負(fù)責(zé)稀釋showElems觸發(fā)的頻率, showElems負(fù)責(zé)元素滾動到視口時的相應(yīng)動作 var throttledScroll = throttle(showElems, 500) window.addEventListener("scroll", throttledScroll)debounce
設(shè)想一些用戶的頻繁操作, 例如滾動, 文本框輸入等, 每次觸發(fā)事件都要調(diào)用回調(diào)函數(shù); 這樣做的代價未免大了點(diǎn). 而debounce的作用就是在在用戶高頻觸發(fā)事件時, 暫時不調(diào)用回調(diào), 在用戶停止高頻操作(例如停止輸入, 停止?jié)L動時), 再調(diào)用一次回調(diào).
解決方案有了, 怎樣用代碼實(shí)現(xiàn)呢? 這里我們要用到setTimeout這個功能來做函數(shù)調(diào)用的延遲. 具體代碼如下(將代碼粘貼到console中執(zhí)行以下, 自己試試看):
var timer; document.addEventListener("scroll", function(){ clearTimeout(timer); //如果操作時已經(jīng)有了延遲執(zhí)行, 則取消該延遲執(zhí)行 timer = setTimeout(function() { //設(shè)定新的延遲執(zhí)行 callback(); }, 500) })
(這里我們?yōu)榱朔奖阏f明, 設(shè)定了timer全局變量. 實(shí)踐中我們可以將timer附加為函數(shù)的屬性, 隱藏在閉包中, 或者作為對象的屬性等. )
當(dāng)第一次高頻操作觸發(fā)時, 設(shè)定一個timer, 在500ms后執(zhí)行; 如果用戶在500ms之內(nèi)沒有再次進(jìn)行該操作(本例中是滾動), 那么我們調(diào)用callback; 然而如果500ms之內(nèi)用戶又觸發(fā)了滾動(即所謂的高頻操作), 那么我們清除上一次設(shè)定的timeout, 設(shè)定一個新的, 500ms之后執(zhí)行的timeout.
大家思考一下, debounce的本質(zhì)就是在用戶觸發(fā)expensive操作時, 不斷延期該expensive操作的執(zhí)行時間(取消和設(shè)定timeout的代價是很小的). 當(dāng)用戶停止操作, 那我們就不再延期, 最后一次設(shè)定的timeout會在500ms后執(zhí)行expensive operation, 例如dom操作, 計(jì)算等.
到這里我們似乎已經(jīng)有了一個解決方案! 然而還有個小小的問題.....
如果用戶不停地操作, 那debounce就會不斷把操作延期, 如果用戶沒有兩次操作的間隔時間大于500ms, 那么我們的callback永遠(yuǎn)也得不到執(zhí)行. 可憐的callback! 恩, 在這一點(diǎn)上我們當(dāng)然可以改進(jìn)...
throttlethrottle的作用是, 保證一個函數(shù)至少間隔一段時間得到一次執(zhí)行. 不像等待用戶停止的debounce, throttle即使在用戶不停操作時, 也能讓callback在操作期間得到間隔的執(zhí)行.
那么該怎么做呢? 一種方法是在用戶開始操作時記錄開始時間, 同時設(shè)定一個flag ifOperationBegin = true. 之后在每次用戶的操作中判斷當(dāng)前時間, 如果當(dāng)前時間-開始時間 > 某個值, 比如500ms, 則執(zhí)行callback, 同時設(shè)定ifOperationBegin = true, 以開始下一次的設(shè)定開始時間 -> 記錄操作時間 -> 判斷的循環(huán). 具體到代碼實(shí)現(xiàn)上:
var scrollBegin = false, scrollStartTime = null; //用戶尚未開始操作 document.addEventListener("scroll", function(){ if(!scrollBegin)scrollStartTime = Date.now();//記錄開始時間, 前提是callback還沒有被觸發(fā)過 scrollBegin = true;//設(shè)定flag if(Date.now() - scrollStartTime > 500){ //如果操作時間和開始時間間隔大于500ms則 exec(elems, cb); //調(diào)用回調(diào) scrollBegin = false; //flag設(shè)為false, 以設(shè)定新的開始時間 } })
這樣做的效果是, 在用戶持續(xù)觸發(fā)scroll操作時, 保證在用戶操作期間callback至少會每隔500ms觸發(fā)一次. 如果用戶操作在500ms之內(nèi)結(jié)束, 那也木有關(guān)系, 下一次用戶重新開始操作時我們的scrollStartTime 依然保留著, callback會被立即觸發(fā).
實(shí)際運(yùn)用那這兩種技術(shù)可以運(yùn)用到哪里呢? 請看如下代碼栗子:
function detectVisible(selector, cb, interval){ //檢測元素是否在視口的函數(shù) var elems = document.querySelectorAll(selector), innerHeight = window.innerHeight; var exec = function(elems, cb){ //回調(diào)函數(shù) Array.prototype.forEach.call(elems, function(elem, index){ if(elem.getBoundingClientRect().top < innerHeight){ //判斷元素是否出現(xiàn)在視口 cb.call(elem, elem); //調(diào)用傳入的回調(diào) } }) } 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);
這個栗子中我們綜合運(yùn)用了throttle和debounce, 達(dá)到了如下效果: 用戶不停滾動時callback會至少每500ms觸發(fā)一次; 用戶停止?jié)L動后的500ms判斷函數(shù)也會觸發(fā)一次. 大家可以打開console查看callback何時是被throttle觸發(fā)的, 何時是被debounce觸發(fā)的.
總結(jié)這一篇文章的主要主題事件的稀釋以期性能上的改善, 有兩種解決方法:throttle和debounce. 前者是通過在用戶操作時判斷操作時間, 來達(dá)到間隔一段時間觸發(fā)回調(diào)的效果; 而后者則是將觸發(fā)的時間不斷延期, 直到用戶停止操作再執(zhí)行回調(diào). 兩者各有優(yōu)缺點(diǎn), 兩相結(jié)合, 我們得到了一個用戶無論怎樣操作(不停操作或者操作時間極短)都可以保證callback定期得到執(zhí)行的函數(shù). problem solved!
看完這篇, 如果你有所收獲, 請去github給我加個star唄!~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/88025.html
摘要:引言前端開發(fā)中一個老生常談的問題就是當(dāng)用戶滾動時根據(jù)滾動的位置適當(dāng)觸發(fā)不同的函數(shù)動畫例如當(dāng)元素出現(xiàn)在視口時觸發(fā)該元素的改變通常的做法就是在上附加事件但是我們知道當(dāng)滾動條滾動時事件觸發(fā)的是很頻繁的且不由控制瀏覽器的事件隊(duì)列原生提供如圖添加事件 引言 前端開發(fā)中一個老生常談的問題就是當(dāng)用戶滾動時, 根據(jù)滾動的位置適當(dāng)觸發(fā)不同的函數(shù)/動畫, 例如當(dāng)元素出現(xiàn)在視口時觸發(fā)該元素的style改變....
摘要:配置項(xiàng)配置項(xiàng)中的參數(shù)有以下三個所監(jiān)聽對象的具體祖先元素,默認(rèn)是計(jì)算交叉狀態(tài)時,將附加到祖先元素上,從而有效的擴(kuò)大或者縮小祖先元素判定區(qū)域設(shè)置一系列的閾值,當(dāng)交叉狀態(tài)達(dá)到閾值時,會觸發(fā)回調(diào)函數(shù)。 一、前言 ??通常情況下,HTML 中的圖片資源會自上而下依次加載,而部分圖片只有在用戶向下滾動頁面的場景下才能被看見,否則這部分圖片的流量就白白浪費(fèi)了。 ??所以,對于那些含有大量圖片資源的網(wǎng)...
摘要:用于獲得當(dāng)前元素到定位父級頂部的距離偏移值。后來在項(xiàng)目中總會遇到滾動吸頂?shù)男Ч枰獙?shí)現(xiàn),現(xiàn)在我將我知道的種滾動吸頂實(shí)現(xiàn)方式做詳細(xì)介紹。有兼容性問題,在微信瀏覽器某些版本中的值會為,于是乎也就有了第三種方案的兼容性寫法。修改版預(yù)覽 這篇文章是三天前寫就的,有大佬給我提了一些修改意見,我覺得這個意見確實(shí)中肯。所以就有了這個升級的修改版本。代碼同步更新到 GitHub 了。 修改內(nèi)容如下: 添加...
摘要:原文鏈接延遲加載也稱為惰性加載,即在長網(wǎng)頁中延遲加載圖像。傳入一個回調(diào)函數(shù),當(dāng)其觀察到元素集合出現(xiàn)時候,則會執(zhí)行該函數(shù)。管理的是一個數(shù)組,當(dāng)元素出現(xiàn)或消失的時候,數(shù)組添加或刪除該元素,并且執(zhí)行該回調(diào)函數(shù)。 原文鏈接 - https://zhuanlan.zhihu.com/p/25455672 延遲加載也稱為惰性加載,即在長網(wǎng)頁中延遲加載圖像。用戶滾動到它們之前,視口外的圖像不會加載。...
閱讀 2188·2023-04-25 19:06
閱讀 1388·2021-11-17 09:33
閱讀 1776·2019-08-30 15:53
閱讀 2599·2019-08-30 14:20
閱讀 3553·2019-08-29 12:58
閱讀 3552·2019-08-26 13:27
閱讀 512·2019-08-26 12:23
閱讀 493·2019-08-26 12:22