函數除顫/節流提升性能 + 原生實現滾動時到視口時展示

引言

前端開發中一個老生常談的問題就是'當用戶滾動時, 根據滾動的位置適當觸發不一樣的函數/動畫, 例如當元素出如今視口時觸發該元素的style改變. 一般的作法就是在scrollElement上附加scoll事件. 可是咱們知道, 當滾動條滾動時scroll事件觸發的是很頻繁的, 且不禁JS控制(瀏覽器的事件隊列原生提供), 如圖(添加事件監聽後滾輪三格):javascript

clipboard.png

依據系統設置的不一樣, 一次滾輪觸發的scroll事件大概在10~15次之間. 若是在回調函數中添加大量的DOM操做或者計算的話, 會引發明顯的卡頓等性能問題. 那有沒有辦法去稀釋回調函數的觸發操做呢? 這個時候就須要函數節流(throttle)和debounce(去顫抖)來解決了!html

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的做用就是在在用戶高頻觸發事件時, 暫時不調用回調, 在用戶中止高頻操做(例如中止輸入, 中止滾動時), 再調用一次回調. 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);

這個栗子中咱們綜合運用了throttledebounce, 達到了以下效果: 用戶不停滾動時callback會至少每500ms觸發一次; 用戶中止滾動後的500ms判斷函數也會觸發一次. 你們能夠打開console查看callback什麼時候是被throttle觸發的, 什麼時候是被debounce觸發的.

總結

這一篇文章的主要主題事件的稀釋以期性能上的改善, 有兩種解決方法:throttledebounce. 前者是經過在用戶操做時判斷操做時間, 來達到間隔一段時間觸發回調的效果; 然後者則是將觸發的時間不斷延期, 直到用戶中止操做再執行回調. 二者各有優缺點, 兩相結合, 咱們獲得了一個用戶不管怎樣操做(不停操做或者操做時間極短)均可以保證callback按期獲得執行的函數. problem solved!

看完這篇, 若是你有所收穫, 請去github給我加個star唄!~

相關文章
相關標籤/搜索