前段時間有朋友問我一個他們公司遇到的問題, 說是後端由於某種原因沒有實現分頁功能, 所以一次性返回了2萬條數據,讓前端用select組件展示到用戶界面裏. 我聽完之後立馬明白了他的困惑, 如果通過硬編碼的方式去直接渲染這兩萬條數據到select中,肯定會卡死. 後面他還說需要支持搜索, 也是前端來實現,我頓時產生了興趣. 當時想到的方案大致如下:
採用懶加載+分頁(前端維護懶加載的數據分發和分頁)
使用虛擬滾動技術(目前react的antd4.0已支持虛擬滾動的select長列表)
懶加載和分頁方式一般用於做長列表優化, 類似於表格的分頁功能, 具體思路就是用戶每次只加載能看見的數據, 當滾動到底部時再去加載下一頁的數據.
虛擬滾動技術也可以用來優化長列表, 其核心思路就是每次只渲染可視區域的列表數,當滾動後動態的追加元素並通過頂部padding來撐起整個滾動內容,實現思路也非常簡單.
通過以上分析其實已經可以解決朋友的問題了,但是最爲一名有追求的前端工程師, 筆者認真梳理了一下,並基於第一種方案抽象出一個實際的問題:
如何渲染大數據列表並支持搜索功能?
筆者將通過模擬不同段位前端工程師的實現方案, 來探索一下該問題的價值. 希望能對大家有所啓發, 學會真正的深入思考.
正文
筆者將通過不同經驗程序員的技術視角來分析以上問題, 接下來開始我們的表演.
在開始代碼之前我們先做好基礎準備, 筆者先用nodejs搭建一個數據服務器, 提供基本的數據請求,核心代碼如下:
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同理).
初級工程師的方案
直接從後端請求數據, 渲染到頁面的硬編碼方案,思路如下:
代碼可能是這樣的:
請求後端數據:
fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
if(res.state) {
data = res.data
setList(data)
}
})
複製代碼
渲染頁面
{
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>
})
}
複製代碼
搜索數據
const handleSearch = (v) => {
let searchData = data.filter((item, i) => {
return item.title.indexOf(v) > -1
})
setList(searchData)
}
複製代碼
這樣做本質上是可以實現基本的需求,但是有明顯的缺點,那就是數據一次性渲染到頁面中, 數據量龐大將導致頁面性能極具降低, 造成頁面卡頓.
中級工程師的方案
作爲一名有一定經驗的前端開發工程師,一定對頁面性能有所瞭解, 所以一定會熟悉防抖函數和節流函數, 並使用過諸如懶加載和分頁這樣的方案, 接下來我們看看中級工程師的方案:
通過這個過程的優化, 代碼已經基本可用了, 下面來介紹具體實現方案:
懶加載+分頁方案 懶加載的實現主要是通過監聽窗口的滾動, 當某一個佔位元素可見之後去加載下一個數據,原理如下:
這裏我們通過監聽window的scroll事件以及對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))
}
}
}
複製代碼
防抖函數實現 防抖函數因爲比較簡單, 這裏直接上一個簡單的防抖函數代碼:
function debounce(fn, time) {
return function(args) {
let that = this
clearTimeout(fn.tid)
fn.tid = setTimeout(() => {
fn.call(that, args)
}, time);
}
}
複製代碼
搜索實現 搜索功能代碼如下:
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的通信機制來通知主進程, 比如模糊搜索等, 我們還可以對搜索算法進一步優化,比如二分法等,所以這些都是高級工程師該考慮的問題. 但是一定要分清場景, 尋找出性價比更高的方案.
最後
如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進技術羣,長期交流學習...
關注公衆號「前端下午茶」,持續爲你推送精選好文,也可以加我爲好友,隨時聊騷。
點個在看支持我吧,轉發就更好了