問題考察點
- 考察前端如何處理大量數據
- 考察大量數據的性能優化
- 考察處理問題的思考方式(關于這一點,文末會說到,大家繼續閱讀)
方案一:直接渲染所有數據
如果請求到10萬條數據直接渲染,頁面會卡死的,很顯然,這種方式是不可取的
- 考察前端如何處理大量數據
- 考察大量數據的性能優化
- 考察處理問題的思考方式(關于這一點,文末會說到,大家繼續閱讀)
async plan() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.arr = res.data.data; this.loading = false; }
方案二:使用定時器分組分批分堆依次渲染(定時加載、分堆思想)
- 正常來說,十萬條數據請求,需要2秒到10秒之間(有可能更長,取決于數據具體內容)
- 而這種方式就是,前端請求到10萬條數據以后,先不著急渲染,先將10萬條數據分堆分批次
- 比如一堆存放10條數據,那么十萬條數據就有一萬堆
- 使用定時器,一次渲染一堆,渲染一萬次即可
- 這樣做的話,頁面就不會卡死了
分組分批分堆函數
- 我們先寫一個函數,用于將10萬條數據進行分堆
- 所謂的分堆其實「思想就是一次截取一定長度的數據」
- 比如一次截取10條數據,頭一次截取09,第二次截取1019等固定長度的截取
- 舉例原來的數據是:[1,2,3,4,5,6,7]
- 假設我們分堆以后,一堆分3個,那么得到的結果就是二維數組了
- 即:[ [1,2,3], [4,5,6], [7]]
- 然后就遍歷這個二維數組,得到每一項的數據,即為每一堆的數據
- 進而使用定時器一點點、一堆堆賦值渲染即可
- 舉例原來的數據是:[1,2,3,4,5,6,7]
#分組分批分堆函數(一堆分10個) function averageFn(arr) { let i = 0; // 1. 從第0個開始截取 let result = []; // 2. 定義結果,結果是二維數組 while (i < arr.length) { // 6. 當索引等于或者大于總長度時,即截取完畢 // 3. 從原始數組的第一項開始遍歷 result.push(arr.slice(i, i + 10)); //4. 在原有十萬條數據上,一次截取10個用于分堆 i = i + 10; //5. 這10條數據截取完,再截取下十條數據,以此類推 } return result; // 7. 最后把結果丟出去即可 }
創建定時器去依次賦值渲染
比如我們每隔一秒鐘去賦值渲染一次
async plan() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.loading = false; let twoDArr = averageFn(res.data.data); for (let i = 0; i < twoDArr.length; i++) { // 相當于在很短的時間內創建許多個定時任務去處理 setTimeout(() => { this.arr = [...this.arr, ...twoDArr[i]]; // 賦值渲染 }, 1000 * i); // 17 * i // 注意設定的時間間隔... 17 = 1000 / 60 } },
這種方式,相當于在很短的時間內創建許多個定時任務去處理,定時任務太多了,也耗費資源啊。
實際上,這種方式就有了大數據量分頁的思想
方案三: 使用requestAnimationFrame替代定時器去做渲染
關于requestAnimationFrame比定時器的優點,大家可以上網查查性能優化關于requestAnimationFrame和使用場景舉例。
反正大家遇到定時器的時候,就可以考慮一下,是否可以使用請求動畫幀進行優化執行渲染?如果使用請求動畫幀的話,就要修改一下代碼寫法了,前面的不變化,plan方法中的寫法變一下即可,注意注釋:
async plan() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.loading = false; // 1. 將大數據量分堆 let twoDArr = averageFn(res.data.data); // 2. 定義一個函數,專門用來做賦值渲染(使用二維數組中的每一項) const use2DArrItem = (page) => { // 4. 從第一項,取到最后一項 if (page > twoDArr.length - 1) { console.log("每一項都獲取完了"); return; } // 5. 使用請求動畫幀的方式 requestAnimationFrame(() => { // 6. 取出一項,就拼接一項(concat也行) this.arr = [...this.arr, ...twoDArr[page]]; // 7. 這一項搞定,繼續下一項 page = page + 1; // 8. 直至完畢(遞歸調用,注意結束條件) use2DArrItem(page); }); }; // 3. 從二維數組中的第一項,第一堆開始獲取并渲染(數組的第一項即索>引為0) use2DArrItem(0); }
方案四: 搭配分頁組件,前端進行分頁(每頁展示一堆,分堆思想)
這種方式,筆者曾經遇到過,當時的對應場景是數據量也就幾十條,后端直接把幾十條數據丟給前端,讓前端去分頁(后端不做分頁的原因是。他當時臨時有事情請假了,所以就前端去做分頁了。)
- 數據量大的情況下,這種方式,也是一種解決方案
- 思路也是在所有數據的基礎上進行截取
- 簡要代碼如下:
getShowTableData() { // 獲取截取開始索引 let begin = (this.pageIndex - 1) * this.pageSize; // 獲取截取結束索引 let end = this.pageIndex * this.pageSize; // 通過索引去截取,從而展示 this.showTableData = this.allTableData.slice(begin, end); }
實際上,這種大任務拆分成許多小任務,這種方式,做法,應用的思想就是分片的方式(時間),在別的場景,比如大文件上傳的時候,也有這種思想,比如一個500MB的大文件,拆分成50個小文件,一個是10MB這樣...至于大文件上傳的文章,那就等筆者有空了再寫唄...
方案五: 表格滾動觸底加載(滾動到底,再加載一堆)
這里重點就是我們需要去判斷,何時滾動條觸底。判斷方式主要有兩種
- scrollTop + clientHeight >= innerHeight
- 或
- new MutationObserver()去觀測
目前市面上主流的一些插件的原理,大致是這兩種。
筆者舉例的這是,是使用的插件v-el-table-infinite-scroll,本質上這個插件是一個自定義指令。對應npm地址:www.npmjs.com/package/el-…
當然也有別的插件,如vue-scroller 等:一個意思,不贅述
注意,觸底加載也是要分堆的,將發請求獲取到的十萬條數據,進行分好堆,然后每觸底一次,就加載一堆即可
在el-table中使用el-table-infinite-scroll指令步驟
安裝,注意版本號(區分vue2和vue3)「cnpm install --save el-table-infinite-scroll@1.0.10」
注冊使用指令插件
// 使用無限滾動插件 import elTableInfiniteScroll from 'el-table-infinite-scroll'; Vue.use(elTableInfiniteScroll);
因為是一個自定義指令,所以直接寫在el-table標簽上即可
<el-table v-el-table-infinite-scroll="load" :data="tableData" > <el-table-column prop="id" label="ID"></el-table-column> <el-table-column prop="name" label="名字"></el-table-column> </el-table> async load() { // 觸底加載,展示數據... },
案例代碼
<template> <div class="box"> <el-table v-el-table-infinite-scroll="load" height="600" :data="tableData" border style="width: 80%" v-loading="loading" element-loading-text="數據量太大啦,客官稍后..." element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.5)" :header-cell-style="{ height: '24px', lineHeight: '24px', color: '#606266', background: '#F5F5F5', fontWeight: 'bold', }" > <el-table-column type="index" label="序"></el-table-column> <el-table-column prop="id" label="ID"></el-table-column> <el-table-column prop="name" label="名字"></el-table-column> <el-table-column prop="value" label="對應值"></el-table-column> </el-table> </div> </template> <script> // 分堆函數 function averageFn(arr) { let i = 0; let result = []; while (i < arr.length) { result.push(arr.slice(i, i + 10)); // 一次截取10個用于分堆 i = i + 10; // 這10個截取完,再準備截取下10個 } return result; } import axios from "axios"; export default { data() { return { allTableData: [], // 初始發請求獲取所有的數據 tableData: [], // 要展示的數據 loading: false }; }, // 第一步,發請求,獲取大量數據,并轉成二維數組,分堆分組分塊存儲 async created() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.allTableData = averageFn(res.data.data); // 使用分堆函數,存放二維數組 // this.originalAllTableData = this.allTableData // 也可以存一份原始值,留作備用,都行的 this.loading = false; // 第二步,操作完畢以后,執行觸底加載方法 this.load(); }, methods: { // 初始會執行一次,當然也可以配置,使其不執行 async load() { console.log("自動多次執行之,首次執行會根據高度去計算要執行幾次合適"); // 第五步,觸底加載相當于把二維數組的每一項取出來用,取完用完時return停止即可 if (this.allTableData.length == 0) { console.log("沒數據啦"); return; } // 第三步,加載的時候,把二維數組的第一項取出來,拼接到要展示的表格數據中去 let arr = this.allTableData[0]; this.tableData = this.tableData.concat(arr); // 第四步,拼接展示以后,再把二維數組的第一項的數據刪除即可 this.allTableData.shift(); }, }, }; </script>
方案六: 使用無限加載/虛擬列表進行展示
1. 什么是虛擬列表?
- 所謂的虛擬列表實際上是「前端障眼法」的一種表現形式。
- 看到的好像所有的數據都渲染了,實際上只渲染「可視區域」的部分罷了
- 有點像我們看電影,我們看的話,是在一塊電影屏幕上,一秒一秒的看(不停的放映)
- 但是實際上電影有倆小時,如果把兩個小時的電影都鋪開的話,那得需要多少塊電影屏幕呢?
- 同理,如果10萬條數據都渲染,那得需要多少dom節點元素呢?
- 所以我們只給用戶看,他「當下能看到的」
- 如果用戶要快進或快退(下拉滾動條或者上拉滾動條)
- 再把對應的內容呈現在電影屏幕上(呈現在可視區域內)
- 這樣就實現了看著像是所有的dom元素每一條數據都有渲染的障眼法效果了
關于前端障眼法,在具體工作中,如果能夠巧妙使用,會大大提升我們的開發效率的
2. 寫一個簡單的虛擬列表
這里筆者直接上代碼,大家復制粘貼即可使用,筆者寫了一些注釋,以便于大家理解。當然也可以去筆者的倉庫中去瞅瞅哦,GitHub倉庫在文末代碼
<template> <!-- 虛擬列表容器,類似“窗口”,窗口的高度取決于一次展示幾條數據 比如窗口只能看到10條數據,一條40像素,10條400像素 故,窗口的高度為400像素,注意要開定位和滾動條 --> <div class="virtualListWrap" ref="virtualListWrap" @scroll="handleScroll" :style="{ height: itemHeight * count + 'px' }" > <!-- 占位dom元素,其高度為所有的數據的總高度 --> <div class="placeholderDom" :style="{ height: allListData.length * itemHeight + 'px' }" ></div> <!-- 內容區,展示10條數據,注意其定位的top值是變化的 --> <div class="contentList" :style="{ top: topVal }"> <!-- 每一條(項)數據 --> <div v-for="(item, index) in showListData" :key="index" class="itemClass" :style="{ height: itemHeight + 'px' }" > {{ item.name }} </div> </div> <!-- 加載中部分 --> <div class="loadingBox" v-show="loading"> <i class="el-icon-loading"></i> <span>loading...</span> </div> </div> </template> <script> import axios from "axios"; export default { data() { return { allListData: [], // 所有的數據,比如這個數組存放了十萬條數據 itemHeight: 40, // 每一條(項)的高度,比如40像素 count: 10, // 一屏展示幾條數據 start: 0, // 開始位置的索引 end: 10, // 結束位置的索引 topVal: 0, // 父元素滾動條滾動,更改子元素對應top定位的值,確保聯動 loading: false, }; }, computed: { // 從所有的數據allListData中截取需要展示的數據showListData showListData: function () { return this.allListData.slice(this.start, this.end); }, }, async created() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.allListData = res.data.data; this.loading = false; }, methods: { // 滾動這里可以加上節流,減少觸發頻次 handleScroll() { /** * 獲取在垂直方向上,滾動條滾動了多少像素距離Element.scrollTop * * 滾動的距離除以每一項的高度,即為滾動到了多少項,當然,要取個整數 * 例:滾動4米,一步長0.8米,滾動到第幾步,4/0.8 = 第5步(取整好計算) * * 又因為我們一次要展示10項,所以知道了起始位置項,再加上結束位置項, * 就能得出區間了【起始位置, 起始位置 + size項數】==【起始位置, 結束位置】 * */ const scrollTop = this.$refs.virtualListWrap.scrollTop; this.start = Math.floor(scrollTop / this.itemHeight); this.end = this.start + this.count; /** * 動態更改定位的top值,確保聯動,動態展示相應內容 * */ this.topVal = this.$refs.virtualListWrap.scrollTop + "px"; }, }, }; </script> <style scoped lang="less"> // 虛擬列表容器盒子 .virtualListWrap { box-sizing: border-box; width: 240px; border: solid 1px #000000; // 開啟滾動條 overflow-y: auto; // 開啟相對定位 position: relative; .contentList { width: 100%; height: auto; // 搭配使用絕對定位 position: absolute; top: 0; left: 0; .itemClass { box-sizing: border-box; width: 100%; height: 40px; line-height: 40px; text-align: center; } // 奇偶行改一個顏色 .itemClass:nth-child(even) { background: #c7edcc; } .itemClass:nth-child(odd) { background: pink; } } .loadingBox { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.64); color: green; display: flex; justify-content: center; align-items: center; } } </style>
3. 使用vxetable插件實現虛擬列表
如果不是列表,是table表格的話,筆者這里推薦一個好用的UI組件,vxetable,看名字就知道做的是表格相關的業務。其中就包括虛擬列表。vue2和vue3版本都支持,性能比較好,官方說:虛擬滾動(最大可以支撐 5w 列、30w 行)強大!
官方網站地址:vxetable.cn/v3/#/table/…
安裝使用代碼
注意安裝版本,筆者使用的版本如下:cnpm i xe-utils vxe-table@3.6.11 --save
「main.js」
// 使用VXETable import VXETable from 'vxe-table' import 'vxe-table/lib/style.css' Vue.use(VXETable)
代碼方面也很簡單,如下:
<template> <div class="box"> <vxe-table border show-overflow ref="xTable1" height="300" :row-config="{ isHover: true }" :loading="loading" > <vxe-column type="seq"></vxe-column> <vxe-column field="id" title="ID"></vxe-column> <vxe-column field="name" title="名字"></vxe-column> <vxe-column field="value" title="對應值"></vxe-column> </vxe-table> </div> </template> <script> import axios from "axios"; export default { data() { return { loading: false, }; }, async created() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.loading = false; this.render(res.data.data); }, methods: { render(data) { this.$nextTick(() => { const $table = this.$refs.xTable1; $table.loadData(data); }); }, }, }; </script>
方案七: 開啟多線程Web Worker進行操作
本案例中,使用Web Worker另外開啟一個線程去操作代碼邏輯,收益并不>是特別大(假如使用虛擬滾動列表插件的情況下)
不過也算是一個拓展的思路吧,倒是可以說一說,提一提。
方案八: 未雨綢繆,防患于未然
以下為筆者愚見,僅供參考...
- 在上述解決方案都說完以后,并沒有結束。
- 實際上本題目在考查候選人知識的廣度和深度以外,更是考查了候選人的處理問題的思考方式,這一點尤其重要!
- 筆者曾做過候選人去求職,也曾做過面試官去面試。就程序員開發工作而言,技術知識點不熟悉,可以快速學習,如文檔、谷歌、百度、技術交流群,相關同事都可提供一定的支持
- 更重要的是看中候選人的思考方式,思維模式。
- 試想,兩個候選人實力水平差不多,但是一個只知道埋頭苦干,有活就干,不去斟酌;而另外一個卻是在用心工作的時候,也會仰望星空,會分析如何干活能夠高性價比地完成任務,注重過程與結果
- 這樣的話,哪個更加受歡迎一些呢?
如果筆者是候選人,筆者在說了上述7種方案以后,會再補充第八種方案:未雨綢繆,防患于未然
場景模擬
面試官隨意打量著其手中我的簡歷,撫須怪叫一聲:“小子,后端要一次性返回10萬條數據給你,你如何處理?”我眉毛一挑,歪嘴一笑:“在上述7種方案陳述完以后,我想類似的問題,我們可以從根本上去解決。即第八種方案,「要未雨綢繆,防患于未然」?!?/p>
“哦?”面試官心中疑惑,緩緩放下我的簡歷:“愿聞其詳?!?/p>
我不緊不慢地答道:“在具體開發工作中,我們在接到一個需求時,在技術評審期間,我們就要和后端去商量比較合適的技術解決方案。這個問題是后端要一次性返回我10萬條數據,重點并不在10萬條這么多數據,而在于后端為什么要這樣做?”
面試官抬頭,認真聽了起來。
我一字一頓地說道:“除去業務真正需要這種方案的話(若是客戶要求的,那就沒啥好說的,干就完了),后端這樣做的原因大致有兩種,第一種他不太懂sql的limit語句,但這基本不可能,第二種就是他有事情,隨便敷衍寫了一下。所以,就是要和他溝通,從大數據量接口請求時長過長,以及過多的dom元素渲染導致性能變差,以及項目的可維護性等角度去溝通,我相信只要正確的溝通,就能從根源上去避免這種不太合理的情況發生?!?/p>
面試官又突然狡黠地發問:“要是溝通以后,后端死活不給你分頁呢?你咋辦?你的溝通無效果!你如何處理!人家不聽你的!”似乎是覺得這個問題很刁鉆,他雙臂抱在胸前,靠在椅背上,等待著我臉上即將綻放的的回答不上來地尷尬笑容。
我內心冷哼一聲:雕蟲小技...
我盯著面試官的眼睛,認真說道:“如果工作中溝通無效果,要么是我自己溝通語言表達的問題,這一點我會注意,不斷提升自己的溝通技巧和說話方式,要么就是...”
我聲音揚起了三分:“我溝通的這個人有問題!他工作摸魚偷懶耍滑!固執己見!為難他人!高高在上!自以為是!這種情況下,我會找到我的直屬領導去介入,因為這已經不是項目的需求問題了,而是員工的基本素養問題!”
停頓了一秒,我聲音又柔和了幾分:“但是,但是我相信咱們公司員工中是絕對沒有這樣的人存在的,各個都是能力強悍,態度端正的優秀員工。畢竟咱們公司在行業中久負盛名,我也是因此慕名而來的。您說對吧?”
面試官眼中閃過震驚之色,他沒有想到我居然把皮球又踢給他了,不過他為了維持形象,旋即恢復了鎮定,只是面部肌肉在止不住的微微顫抖。
我又補充道:“實際上在工作中,前端作為比較貼近用戶的角色而言,需要和各個崗位的同事進行溝通,比如后端、產品、UI、測試等。我們需要通過合理的溝通方式,去提升工作效率,完成項目,實現自己的價值,為公司創造收益,我想這是每一個員工需要做的,也是必須要做到的?!?/p>
面試官又撫須怪叫一聲:“小子表現還行,你被錄用了!一個月工資2200,自帶電腦,無社無金,007工作制,不能偷吃公司零食,以及...”
我:阿噠...
總結
有效的溝通,源自于解決問題的思維模式,在多數情況下,重要性,大于當下所掌握的技術知識點