前段時間有朋友問我一個他們公司遇到的問題, 說是后端由于某種原因沒有實現分頁功能, 所以一次性返回了2萬條數據,讓前端用select組件展示到用戶界面里. 我聽完之后立馬明白了他的困惑, 如果通過硬編碼的方式去直接渲染這兩萬條數據到select中,肯定會卡死. 后面他還說需要支持搜索, 也是前端來實現,我頓時產生了興趣. 當時想到的方案大致如下:
采用懶加載+分頁(前端維護懶加載的數據分發和分頁)
使用虛擬滾動技術(目前react的antd4.0已支持虛擬滾動的select長列表)
懶加載和分頁方式一般用于做長列表優化, 類似于表格的分頁功能, 具體思路就是用戶每次只加載能看見的數據, 當滾動到底部時再去加載下一頁的數據.
虛擬滾動技術也可以用來優化長列表, 其核心思路就是每次只渲染可視區域的列表數,當滾動后動態的追加元素并通過頂部padding來撐起整個滾動內容,實現思路也非常簡單.
通過以上分析其實已經可以解決朋友的問題了,但是最為一名有追求的前端工程師, 筆者認真梳理了一下,并基于第一種方案抽象出一個實際的問題:
如何渲染大數據列表并支持搜索功能?
筆者將通過模擬不同段位前端工程師的實現方案, 來探索一下該問題的價值. 希望能對大家有所啟發, 學會真正的深入思考.
正文
筆者將通過不同經驗程序員的技術視角來分析以上問題, 接下來開始我們的表演.
在開始代碼之前我們先做好基礎準備, 筆者先用nodejs搭建一個數據服務器, 提供基本的數據請求,核心代碼如下:
app.use(async(ctx, next) => {
if(ctx.url ==='/api/getMock') {
letlist = []
// 生成指定個數的隨機字符串
functiongenrateRandomWords(n) {
letwords ='abcdefghijklmnopqrstuvwxyz你是好的嗯氣短前端后端設計產品網但考慮到付款啦分手快樂的分類開發商的李開復封疆大吏師德師風吉林省附近',
len = words.length,
ret =''
for(leti=0; i< n; i++) {
ret += words[Math.floor(Math.random() * len)]
}
returnret
}
// 生成10萬條數據的list
for(leti =0; i<100000; i++) {
list.push({
name:`xu_0${i}`,
title: genrateRandomWords(12),
text:`我是第${i}項目, 趕快??吧~~`,
tid:`xx_${i}`
})
}
ctx.body = {
state:200,
data: list
}
}
awaitnext()
})
復制代碼
以上筆者是采用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
{item.title}{item.name}</span></div>
<div>{item.text}</div>
</div>
})
}
復制代碼
搜索數據
consthandleSearch = (v) => {
letsearchData = data.filter((item, i) => {
returnitem.title.indexOf(v) >-1
})
setList(searchData)
}
復制代碼
這樣做本質上是可以實現基本的需求,但是有明顯的缺點,那就是數據一次性渲染到頁面中, 數據量龐大將導致頁面性能極具降低, 造成頁面卡頓.
中級工程師的方案
作為一名有一定經驗的前端開發工程師,一定對頁面性能有所了解, 所以一定會熟悉防抖函數和節流函數, 并使用過諸如懶加載和分頁這樣的方案, 接下來我們看看中級工程師的方案:
通過這個過程的優化, 代碼已經基本可用了, 下面來介紹具體實現方案:
懶加載+分頁方案 懶加載的實現主要是通過監聽窗口的滾動, 當某一個占位元素可見之后去加載下一個數據,原理如下:
這里我們通過監聽window的scroll事件以及對poll元素使用getBoundingClientRect來獲取poll元素相對于可視窗口的距離, 從而自己實現一個懶加載方案.
在滾動的過程匯總我們還需要注意一個問題就是當用戶往回滾動時, 實際上是不需要做任何處理的,所以我們需要加一個單向鎖, 具體代碼如下:
functionscrollAndLoading() {
if(window.scrollY > prevY) {// 判斷用戶是否向下滾動
prevY =window.scrollY
if(poll.current.getBoundingClientRect().top <=window.innerHeight) {
// 請求下一頁數據
}
}
}
useEffect(() => {
// something code
constgetData = debounce(scrollAndLoading,300)
window.addEventListener('scroll', getData,false)
return() => {
window.removeEventListener('scroll', getData,false)
}
}, [])
復制代碼
其中prevY存儲的是窗口上一次滾動的距離, 只有在向下滾動并且滾動高度大于上一次時才更新其值.
至于分頁的邏輯,原生javascript實現分頁也很簡單, 我們通過定義幾個維度:
curPage當前的頁數
pageSize 每一頁展示的數量
data 傳入的數據量
有了這幾個條件,我們的基本能分頁功能就可以完成了. 前端分頁的核心代碼如下:
letdata = [];
letcurPage =1;
letpageSize =16;
letprevY =0;
// other code...
functionscrollAndLoading() {
if(window.scrollY > prevY) {// 判斷用戶是否向下滾動
prevY =window.scrollY
if(poll.current.getBoundingClientRect().top <=window.innerHeight) {
curPage++
setList(searchData.slice(0, pageSize * curPage))
}
}
}
復制代碼
防抖函數實現防抖函數因為比較簡單, 這里直接上一個簡單的防抖函數代碼:
functiondebounce(fn, time) {
returnfunction(args) {
letthat =this
clearTimeout(fn.tid)
fn.tid = setTimeout(() => {
fn.call(that, args)
}, time);
}
}
復制代碼
搜索實現 搜索功能代碼如下:
consthandleSearch = (v) => {
curPage =1;
prevY =0;
searchData = data.filter((item, i) => {
// 采用正則來做匹配, 后期支持前端模糊搜索
letreg =newRegExp(v,'gi')
returnreg.test(item.title)
})
setList(searchData.slice(0, pageSize * curPage))
}
復制代碼
需要結合分頁來實現, 所以這里為了不影響源數據, 我們采用臨時數據searchData來存儲. 效果如下:
搜索后:
無論是搜索前還是搜索后, 都利用了懶加載, 所以再也不用擔心數據量大帶來的性能瓶頸了~
高級工程師的方案
作為一名久經戰場的程序員, 我們應該考慮更優雅的實現方式,比如組件化, 算法優化, 多線程這類問題, 就比如我們問題中的大數據渲染, 我們也可以用虛擬長列表來更優雅簡潔的來解決我們的需求. 至于虛擬長列表的實現筆者在開頭已經點過,這里就不詳細介紹了, 對于更大量的數據,比如100萬(雖然實際開發中不會遇到這么無腦的場景),我們又該怎么處理呢?
第一個點我們可以使用js緩沖器來分片處理100萬條數據, 思路代碼如下:
functionmultistep(steps,args,callback){
vartasks = steps.concat();
setTimeout(function(){
vartask = 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的通信機制來通知主進程, 比如模糊搜索等, 我們還可以對搜索算法進一步優化,比如二分法等,所以這些都是高級工程師該考慮的問題. 但是一定要分清場景, 尋找出性價比更高的方案.