當下拉選單資料過大時,該如何應對?

no-simple發表於2018-11-09

前言

後面有程式碼以及demo地址

在日常開發中,除了現成外掛的使用外,還有很多問題是隻能自己動手的。先丟擲問題,當一個下拉選單的資料達到幾千條甚至上萬,這個時候瀏覽器已經會出現嚴重卡頓了。看看下面的例子

demo

如圖所示,資料量達到2W簡單測試資料(頁面沒有其他東西),點選載入下拉選單花了大概5s時間。出現這種情況心裡真的是很複雜,這不是在玩我嗎?

當下拉選單資料過大時,該如何應對?

解決思路

這個問題其實和表格資料是同一個效能問題,表格的解決方式是通過分頁器來減少頁面承載的資料量。那麼下拉選單該如何解決呢?通常我們都是一次性載入下拉的所有資料的,針對目前的難題,思路也是一樣,採用分頁來解決頁面的效能問題。問題又來了,分頁器是可以點選的,那下拉選單又不可以點選,那就只有在監聽滾動事件裡實現這件大事了。

先來大綱:

  1. 監聽滾動
  2. 向下滾動時往後載入資料
  3. 向上滾動時往前載入資料
  4. 資料有進有出
    當下拉選單資料過大時,該如何應對?

好戲開始

1. 監聽滾動

    <el-select class="remoteSelect" v-scroll v-model="value">
      <el-option :value="item.id" v-for="item in list" :key="item.id">{{item.name}}</el-option>
    </el-select>
複製程式碼

這裡是基於vue與element-ui中el-select實現的監聽滾動。這裡是採用自定義指令的方式監聽滾動

// directives目錄下index.js檔案

import Vue from 'vue'
export default () => {
  Vue.directive('scroll', {
    bind (el, binding) {
      // 獲取滾動頁面DOM
      let SCROLL_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
      SCROLL_DOM.addEventListener('scroll', function () {
        console.log('scrll')
      })
    }
  })
}
複製程式碼

在main.js中通過全域性方法Vue.use()註冊使用

import Directives from './directives'
Vue.use(Directives)
複製程式碼

這時滾動頁面就可以看到控制的列印日誌,代表監聽已生效,接下來擼起袖子開幹

當下拉選單資料過大時,該如何應對?

2. 向下滾動時往後載入資料

首先要先判斷出是向上滾動,還是向下滾動

  1. 記錄上一次的滾動位置
  2. 當前位置與上一次的滾動位置作比較

通過一個公共變數來記錄全域性位置,通過scrollTop方法獲取當前的滾動位置,並記錄在公共變數scrollPosition

    bind (el, binding) {
      // 獲取滾動頁面DOM
      let SCROLL_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
      let scrollPosition = 0
      SCROLL_DOM.addEventListener('scroll', function () {
        // 當前的滾動位置 減去  上一次的滾動位置
        // 如果為true則代表向上滾動,false代表向下滾動
        let flagToDirection = this.scrollTop - scrollPosition > 0
        // 記錄當前的滾動位置
        scrollPosition = this.scrollTop
        console.log(flagToDirection ? '滾動方向:下' : '滾動方向:上')
      })
    }
複製程式碼

當下拉選單資料過大時,該如何應對?

目前已知曉滾動的方向,接下來便根據滾動方向做相應的處理。將滾動行為告訴元件

    ...省略
        // 記錄當前的滾動位置
        scrollPosition = this.scrollTop
        // 將滾動行為告訴元件
        binding.value(flagToDirection)
複製程式碼

事件接受v-scroll指令中接受事件v-scroll="handleScroll",在該方法handleScroll處理滾動行為。 接下來只需要在該事件中針對為向下的滾動發起請求資料即可

    /*********************************
      ** Fn: handleScroll
      ** Intro: 處理滾動行為
      ** @params: param 為true代表向下滾動
      ** @params: param 為false代表向上滾動
    *********************************/
    handleScroll (param) {
      if (param) {
        // 請求下一頁的資料
        this.list.push(...this.ajaxData(++this.pageIndex))
      }
    },
複製程式碼

當下拉選單資料過大時,該如何應對?

到這裡滾動載入已經實現。只是載入太頻繁了,如果快速滾動則會同時發出多個請求後臺資料,在密集一些遊覽器中ajax就要開發併發排隊了,可見並不理想。那如何控制呢?那換種方式觸發handleScroll事件,在滾動位置距離滾動頁面底部一定高度時在觸發,例如距頁面底部只有100px時觸發handleScroll事件

  1. scrollHeight獲取滾動高度
  2. 在距底部100px時
        // 記錄當前的滾動位置
        scrollPosition = this.scrollTop
        const LIMIT_BOTTOM = 100
        // 記錄滾動位置距離底部的位置
        let scrollBottom = this.scrollHeight - (this.scrollTop + this.clientHeight) < LIMIT_BOTTOM
        // 如果已達到指定位置則觸發
        if (scrollBottom) {
          // 將滾動行為告訴元件
          binding.value(flagToDirection)
        }
複製程式碼

當下拉選單資料過大時,該如何應對?

通過資料長度的變化可以知道觸發事件已經明顯和諧了很多,這種效果很手機懶載入的方式一樣,資料會被不斷的疊加。

小提示: 會存在一個bug,即ajax是非同步的,如果這個ajax請求花了1s才返回資料,而此時還在繼續往下滾,那就會觸發多個請求事件。如何避免這種情況呢? 答案是增加一個標誌位,在請求前將該標誌位設定為false,請求結束後設定為true。每次請求時先判斷該標誌位。如果為false則阻止該事件。

中場

再來看看我們的大綱

  1. 監聽滾動
  2. 向下滾動時往後載入資料
  3. 向上滾動時往前載入資料
  4. 資料有進有出

到這裡我們只完成①和②兩個步驟。如果已經滿足了你的需求,那你可以結束閱讀了。如果對你有那麼一點點幫助,先點個贊在離開。

前面說的都還只是基礎操作,還沒開始劃重點呢。說好的無效能壓力呢?

當下拉選單資料過大時,該如何應對?

  1. 程式碼地址:github
  2. 當前版本demo:demo

先下班回家吃飯吧。週末繼續寫完 --2018-11-09 18:15


華麗的分割


就像週末一樣,一切都會如期而至。--2018-11-10 08:30

3. 向上滾動時往前載入資料

handleScroll中判斷引數param我們就得知了滾動行為,但之前我們只限制了向下滾動的觸發時機,現在完善向上的滾動觸發時機。同樣的,先採用距離頂部100px的時候觸發。

只要當前的滾動位置scrollTop小於100px就觸發handleScroll事件

        // 如果向下滾動 並且距離底部只有100px
        if (flagToDirection && scrollBottom) {
          // 將滾動行為告訴元件
          binding.value(flagToDirection)
        }
        // 如果是向上滾動  並且距離頂部只有100px
        if (!flagToDirection && this.scrollTop < LIMIT_BOTTOM) {
          binding.value(flagToDirection)
        }
複製程式碼

handleScroll事件中我們就已經能檢測到向上滾動行為了,並且觸發時機也符合預期。

當下拉選單資料過大時,該如何應對?
問題接踵而至,還一個比一個嚴重。一直向下滾動時分頁載入則一直在累加,從第一頁到一直滾動載入的頁的資料都在列表裡面了,那為何還需要向上載入呢?這裡先埋下一個坑,先把4. 資料有進有出 看完這個坑就迎刃而解了。

4. 資料有進有出

說好的無效能壓力呢?就在這個關鍵點了。看圖一目瞭然(找這個效果圖不容易呀):

  1. 向下滾動(圖中每次點選即代表一次觸發滾動載入資料)

當下拉選單資料過大時,該如何應對?
2. 向上滾動

當下拉選單資料過大時,該如何應對?
3. 有進有出

當下拉選單資料過大時,該如何應對?
如上圖效果,這就是我們最終要達成的目的。向上滾動我們就載入上一頁的資料,向下滾動就載入下一頁的滾動。資料實體list始終只有一定的資料量,資料量再大又能奈我何呢?

當下拉選單資料過大時,該如何應對?

還是來看看如何實現吧

如何維持這個陣列的長度呢?說起來有進有出很簡單,但實現還是不簡單的。

假設我們現在的陣列容器最多容納4頁的資料量,每頁100條資料。通過pageLimit引數來限定我們需要維護的陣列長度,這裡設為4。

當向下滾動或向上滾動時我們如何知道當前該載入那一頁了?

所以我們需要一個記錄表pageMap來記錄頁碼,該頁碼錶與當前的資料實體list對應。如下的對應關係。

pageLimit: 4
pageMap: [1, 2, 3, 4]
list:['第一頁的資料', '第二頁的資料', '第三頁的資料', '第四頁的資料']

複製程式碼

效果圖(目前滾動不科學,步驟正確,後面有優化滾動):

當下拉選單資料過大時,該如何應對?

    /*********************************
      ** Fn: handleScroll
      ** Intro: 處理滾動行為
      ** @params: param 為true代表向下滾動
      ** @params: param 為false代表向上滾動
    *********************************/
    handleScroll (param) {
      if (param) {
        if (this.pageMap.length >= this.pageLimit) {
          // 當長度相等的時候, 絕對不能超出長度  則有進必有出
          // 刪除 pageMap 列表的第一個元素
          this.pageMap.shift()
          // 對應刪除list中一頁的資料量
          this.list.splice(0, this.pageSize)
        }
        ++this.pageIndex
        this.pageMap.push(this.pageIndex)
        // 請求下一頁的資料
        this.list.push(...this.ajaxData(this.pageIndex))
        // 同步記錄頁碼
      } else {
        // 如果在向上滾動時,如果還沒有到達第一頁則繼續載入。 如果已到達則停止載入
        if (this.pageMap[0] > 1) {
          // 向上滾動,取出pageMap中第一個元素值減1
          this.pageIndex = this.pageMap[0] - 1
          // 同步設定分頁
          // ①先刪除最後一個元素
          this.pageMap.pop()
          // ②將新元素新增在頭部
          this.pageMap = [this.pageIndex, ...this.pageMap]
          // ①刪除list中最後一頁的資料
          this.list.splice(-this.pageSize, this.pageSize)
          // ②將新資料新增在頭部位置
          this.list = [...this.ajaxData(this.pageIndex), ...this.list]
        } else return false
      }
    }
複製程式碼

先寫到這裡吧,又該吃午飯了 2018-11.10 12:01

當下拉選單資料過大時,該如何應對?
下午好,冬天的太陽暖洋洋的~ 2018-11-10 13:04

優化滾動

接下來我們們來填一下上面留下的坑,當資料達到指定長度時,資料總量不會變了,那麼滾動的總體高度scrollHeight也就固定了,這是資料雖然有進有出,但是對滾動位置scrollTop將不再有影響,如上面的動態圖中效果,將會一滾到底,而此時卻還不是分頁的終點,卻讓使用者誤以為到底了~~ 這個問題有點嚴重,有點嚴重~

當下拉選單資料過大時,該如何應對?

優化方法:

  1. 每次載入資料後將當前滾動位置回到總體滾動高度的中間位置。 此時我們需要將滾動dom以及中間位置的高度通過自定義指令v-scroll丟擲來,在往頭部新增資料或尾部新增資料時滾動位置定位到中間位置。

丟擲DOM和滾動的中間位置

// directives目錄下index.js檔案
        // 如果向下滾動 並且距離底部只有100px
        if (flagToDirection && scrollBottom) {
          // 將滾動行為告訴元件
          binding.value(flagToDirection, SCROLL_DOM, this.scrollHeight / 2)
        }
        // 如果是向上滾動  並且距離頂部只有100px
        if (!flagToDirection && this.scrollTop < LIMIT_BOTTOM) {
          binding.value(flagToDirection, SCROLL_DOM, this.scrollHeight / 2)
        }
複製程式碼

pageMap(對應list長度)達到pageLimit長度時,進出增刪資料時重置DOM滾動位置

    /*********************************
      ** Fn: handleScroll
      ** Intro: 處理滾動行為
      ** @params: param 為true代表向下滾動 為false代表向上滾動
      ** @params: el 滾動DOM
      ** @params: middlePosition 滾動列表的中間位置
    *********************************/
    handleScroll (param, el, middlePosition) {
      if (param) {
        if (this.pageMap.length >= this.pageLimit) {
          ....省略程式碼
          this.list.splice(0, this.pageSize)
          // 回滾到中間位置
          el.scrollTop = middlePosition
        }
        ....省略程式碼
      } else {
        // 如果在向上滾動時,如果還沒有到達第一頁則繼續載入。 如果已到達則停止載入
        if (this.pageMap[0] > 1) {
            ....省略程式碼
          this.list = [...this.ajaxData(this.pageIndex), ...this.list]
          // 回滾到中間位置
          el.scrollTop = middlePosition
        } else return false
      }
    },
複製程式碼

當下拉選單資料過大時,該如何應對?
效果如上圖所示,應該要結尾了?仔細觀察的同學又發現彩蛋了。在滾動跳躍的一瞬間,原來使用者看到的資料由於跳動後資料不在是原來使用者看到的資料了,呀呀呀 ..... 這個問題有點嚴重,得慌

當下拉選單資料過大時,該如何應對?
2. 優化滾動臨界點 臨界點即距離滾動總體高度頂部或底部一定距離時,觸發handleScroll的臨界點,即常量LIMIT_BOTTOM。之前定義的const LIMIT_BOTTOM = 100為100,這個是沒啥道理,那麼來個正經的臨界點。

條件梳理

  1. 每次回到到 1/2 scrollHeight的位置
  2. 每次資料的變化位置為 (1 / pageLimit) * scrollHeight,這裡演示的是1/4 * scrollHeight
  3. 設定一個未知數 X 為跳躍的臨界點
  4. 臨界點是使用者在跳躍前看到的位置
  5. 1/2 scrollHeight是使用者跳躍之後的位置

表示式: x + (1/4 * scrollHeight) = (1/2 scrollHeight)

x = (1/4 * scrollHeight),即 const LIMIT_BOTTOM = this.scrollHeight / 4 那我們再開看看滾動情況:

當下拉選單資料過大時,該如何應對?
效果已經差不多了,如果想使用者最後看到位置在靠下一些,可以設定const LIMIT_BOTTOM = this.scrollHeight / 4.2

結語

故事到這裡終於結束了。點個贊 給個鼓勵咯~

在github新建一個倉庫來上傳程式碼:

  1. demo檢視:demo
  2. github程式碼檢視:傳送門
  3. 這篇很實用呀,則沒人欣賞呢Vue自定義指令實現input限制輸入正整數
  4. 為何要再封裝 AJAX?
  • 版權說明:本文首發於掘金,如需轉載請註明出處。

種一棵樹最好的時間是十年前,其次是現在。 --誰說的不重要。

相關文章