當後端一次性丟給你10萬條數據, 做爲前端工程師的你,要怎麼處理?

前段時間有朋友問我一個他們公司遇到的問題, 說是後端因爲某種緣由沒有實現分頁功能, 因此一次性返回了2萬條數據,讓前端用select組件展現到用戶界面裏. 我聽完以後立馬明白了他的困惑, 若是經過硬編碼的方式去直接渲染這兩萬條數據到select中,確定會卡死. 後面他還說須要支持搜索, 也是前端來實現,我頓時產生了興趣. 當時想到的方案大體以下:javascript

  1. 採用懶加載+分頁(前端維護懶加載的數據分發和分頁)
  2. 使用虛擬滾動技術(目前react的antd4.0已支持虛擬滾動的select長列表)

懶加載和分頁方式通常用於作長列表優化, 相似於表格的分頁功能, 具體思路就是用戶每次只加載能看見的數據, 當滾動到底部時再去加載下一頁的數據.前端

虛擬滾動技術也能夠用來優化長列表, 其核心思路就是每次只渲染可視區域的列表數,當滾動後動態的追加元素並經過頂部padding來撐起整個滾動內容,實現思路也很是簡單.vue

經過以上分析其實已經能夠解決朋友的問題了,可是最爲一名有追求的前端工程師, 筆者認真梳理了一下,並基於第一種方案抽象出一個實際的問題:java

如何渲染大數據列表並支持搜索功能?node

筆者將經過模擬不一樣段位前端工程師的實現方案, 來探索一下該問題的價值. 但願能對你們有所啓發, 學會真正的深刻思考.react

正文

筆者將經過不一樣經驗程序員的技術視角來分析以上問題, 接下來開始咱們的表演.程序員

在開始代碼以前咱們先作好基礎準備, 筆者先用nodejs搭建一個數據服務器, 提供基本的數據請求,核心代碼以下:web

app.use(async (ctx, next) => {
  if(ctx.url === '/api/getMock') {
    let list = []
    
    // 生成指定個數的隨機字符串
    function genrateRandomWords(n) {
      let words = 'abcdefghijklmnopqrstuvwxyz你是好的嗯氣短前端後端設計產品網但考慮到付款啦分手快樂的分類開發商的李開復封疆大吏師德師風吉林省附近',
          len = words.length,
          ret = ''
      for(let i=0; i< n; i++) {
        ret += words[Math.floor(Math.random() * len)]
      }
      return ret
    }

    // 生成10萬條數據的list
    for(let i = 0; i< 100000; i++) {
      list.push({
        name: `xu_0${i}`,
        title: genrateRandomWords(12),
        text: `我是第${i}項目, 趕快🌀吧~~`,
        tid: `xx_${i}`
      })
    }

    ctx.body = {
      state: 200,
      data: list
    }
  }
  await next()
})
複製代碼

以上筆者是採用koa實現的基本的mock數據服務器, 這樣咱們就能夠模擬真實的後端環境來開始咱們的前端開發啦(固然也能夠直接在前端手動生成10萬條數據). 其中genrateRandomWords方法用來生成指定個數的字符串,這在mock數據技術中應用不少, 感興趣的盆友能夠學習瞭解一下. 接下來的前端代碼筆者統一採用react來實現(vue同理).算法

初級工程師的方案

直接從後端請求數據, 渲染到頁面的硬編碼方案,思路以下: json

代碼多是這樣的:

  1. 請求後端數據:
fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
  if(res.state) {
    data = res.data
    setList(data)
  }
})
複製代碼
  1. 渲染頁面
{
    list.map((item, i) => {
      return <div className={styles.item} key={item.tid}> <div className={styles.tit}>{item.title} <span className={styles.label}>{item.name}</span></div> <div>{item.text}</div> </div>
    })
}
複製代碼
  1. 搜索數據
const handleSearch = (v) => {
    let searchData = data.filter((item, i) => {
      return item.title.indexOf(v) > -1
     })
     setList(searchData)
  }
複製代碼

這樣作本質上是能夠實現基本的需求,可是有明顯的缺點,那就是數據一次性渲染到頁面中, 數據量龐大將致使頁面性能極具下降, 形成頁面卡頓.

中級工程師的方案

做爲一名有必定經驗的前端開發工程師,必定對頁面性能有所瞭解, 因此必定會熟悉防抖函數節流函數, 並使用過諸如懶加載分頁這樣的方案, 接下來咱們看看中級工程師的方案:

經過這個過程的優化, 代碼已經基本可用了, 下面來介紹具體實現方案:

  1. 懶加載+分頁方案 懶加載的實現主要是經過監聽窗口的滾動, 當某一個佔位元素可見以後去加載下一個數據,原理以下:
    這裏咱們經過監聽windowscroll事件以及對poll元素使用getBoundingClientRect來獲取poll元素相對於可視窗口的距離, 從而本身實現一個懶加載方案.

在滾動的過程彙總咱們還須要注意一個問題就是當用戶往回滾動時, 其實是不須要作任何處理的,因此咱們須要加一個單向鎖, 具體代碼以下:

function scrollAndLoading() {
    if(window.scrollY > prevY) {  // 判斷用戶是否向下滾動
      prevY = window.scrollY
      if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
        // 請求下一頁數據
      }
    }
}

useEffect(() => {
    // something code
    const getData = debounce(scrollAndLoading, 300)
    window.addEventListener('scroll', getData, false)
    return () => {
      window.removeEventListener('scroll', getData, false)
    }
  }, [])
複製代碼

其中prevY存儲的是窗口上一次滾動的距離, 只有在向下滾動而且滾動高度大於上一次時才更新其值.

至於分頁的邏輯, 原生javascript實現分頁也很簡單, 咱們經過定義幾個維度:

  • curPage當前的頁數
  • pageSize 每一頁展現的數量
  • data 傳入的數據量

有了這幾個條件,咱們的基本能分頁功能就能夠完成了. 前端分頁的核心代碼以下:

let data = [];
let curPage = 1;
let pageSize = 16;
let prevY = 0;

// other code...

function scrollAndLoading() {
    if(window.scrollY > prevY) {  // 判斷用戶是否向下滾動
      prevY = window.scrollY
      if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
        curPage++
        setList(searchData.slice(0, pageSize * curPage))
      }
    }
}
複製代碼
  1. 防抖函數實現 防抖函數由於比較簡單, 這裏直接上一個簡單的防抖函數代碼:
function debounce(fn, time) {
    return function(args) {
      let that = this
      clearTimeout(fn.tid)
      fn.tid = setTimeout(() => {
        fn.call(that, args)
      }, time);
    }
  }
複製代碼
  1. 搜索實現 搜索功能代碼以下:
const handleSearch = (v) => {
     curPage = 1;
     prevY = 0;
     searchData = data.filter((item, i) => {
        // 採用正則來作匹配, 後期支持前端模糊搜索
       let reg = new RegExp(v, 'gi')
       return reg.test(item.title)
     })
     setList(searchData.slice(0, pageSize * curPage))
}
複製代碼

須要結合分頁來實現, 因此這裏爲了避免影響源數據, 咱們採用臨時數據searchData來存儲. 效果以下:

搜索後:
不管是搜索前仍是搜索後, 都利用了懶加載, 因此不再用擔憂數據量大帶來的性能瓶頸了~

高級工程師的方案

做爲一名久經戰場的程序員, 咱們應該考慮更優雅的實現方式,好比組件化, 算法優化, 多線程這類問題, 就好比咱們問題中的大數據渲染, 咱們也能夠用虛擬長列表來更優雅簡潔的來解決咱們的需求. 至於虛擬長列表的實現筆者在開頭已經點過,這裏就不詳細介紹了, 對於更大量的數據,好比100萬(雖然實際開發中不會遇到這麼無腦的場景),咱們又該怎麼處理呢?

第一個點咱們可使用js緩衝器來分片處理100萬條數據, 思路代碼以下:

function multistep(steps,args,callback){
    var tasks = steps.concat();

    setTimeout(function(){
        var task = tasks.shift();
        task.apply(null, args || []);   //調用Apply參數必須是數組

        if(tasks.length > 0){
            setTimeout(arguments.callee, 25);
        }else{
            callback();
        }
    },25);
}
複製代碼

這樣就能比較大量計算致使的js進程阻塞問題了.更多性能優化方案能夠參考筆者以前的文章:

咱們還能夠經過web worker來將須要在前端進行大量計算的邏輯移入進去, 保證js主進程的快速響應, 讓web worker線程在後臺計算, 計算完成後再經過web worker的通訊機制來通知主進程, 好比模糊搜索等, 咱們還能夠對搜索算法進一步優化,好比二分法等,因此這些都是高級工程師該考慮的問題. 可是必定要分清場景, 尋找出性價比更高的方案.

最後

若是想學習更多前端技能,實戰學習路線, 歡迎在公衆號《趣談前端》加入咱們的技術羣一塊兒學習討論,共同探索前端的邊界。

更多推薦

相關文章
相關標籤/搜索