什麼?後端要一次性返回我10萬條資料!且看我這8種方案機智應對!

水冗水孚發表於2023-02-28

問題描述

  • 面試官:後端一次性返回10萬條資料給你,你如何處理?
  • 我:歪嘴一笑,what the f**k!

問題考察點

看似無厘頭的問題,實際上考查候選人知識的廣度和深度,雖然在工作中這種情況很少遇到...

  • 考察前端如何處理大量資料
  • 考察候選人對於大量資料的效能最佳化
  • 考察候選人處理問題的思考方式(關於這一點,文末會說到,大家繼續閱讀)
  • ......

文末會提供完整程式碼,供大家更好的理解

使用express建立一個十萬條資料的介面

若是道友對express相關不太熟悉的話,有空可以看看筆者的這一篇全棧文章(還有完整程式碼哦):《Vue+Express+Mysql全棧專案之增刪改查、分頁排序匯出表格功能》
route.get("/bigData", (req, res) => {
  res.header('Access-Control-Allow-Origin', '*'); // 允許跨域
  let arr = [] // 定義陣列,存放十萬條資料
  for (let i = 0; i < 100000; i++) { // 迴圈新增十萬條資料
    arr.push({
      id: i + 1,
      name: '名字' + (i + 1),
      value: i + 1,
    })
  }
  res.send({ code: 0, msg: '成功', data: arr }) // 將十萬條資料返回之
})

點選按鈕,發請求,獲取資料,渲染到表格上

html結構如下:

<el-button :loading="loading" @click="plan">點選請求載入</el-button>

<el-table :data="arr">
  <el-table-column type="index" label="序" />
  <el-table-column prop="id" label="ID" />
  <el-table-column prop="name" label="名字" />
  <el-table-column prop="value" label="對應值" />
</el-table>

data() {
    return {
      arr: [],
      loading: false,
    };
},

async plan() {
    // 發請求,拿資料,賦值給arr
}

方案一 直接渲染所有資料

如果請求到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條資料,頭一次擷取0~9,第二次擷取10~19等固定長度的擷取
  • 舉例原來的資料是:[1,2,3,4,5,6,7]
  • 假設我們分堆以後,一堆分3個,那麼得到的結果就是二維陣列了
  • 即:[ [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地址:https://www.npmjs.com/package...

當然也有別的外掛,如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>

效果圖

方案六 使用無限載入/虛擬列表進行展示

什麼是虛擬列表?

  • 所謂的虛擬列表實際上是前端障眼法的一種表現形式。
  • 看到的好像所有的資料都渲染了,實際上只渲染可視區域的部分罷了
  • 有點像我們看電影,我們看的話,是在一塊電影螢幕上,一秒一秒的看(不停的放映)
  • 但是實際上電影有倆小時,如果把兩個小時的電影都鋪開的話,那得需要多少塊電影螢幕呢?
  • 同理,如果10萬條資料都渲染,那得需要多少dom節點元素呢?
  • 所以我們只給使用者看,他當下能看到的
  • 如果使用者要快進或快退(下拉捲軸或者上拉捲軸)
  • 再把對應的內容呈現在電影螢幕上(呈現在可視區域內)
  • 這樣就實現了看著像是所有的dom元素每一條資料都有渲染的障眼法效果了
關於前端障眼法,在具體工作中,如果能夠巧妙使用,會大大提升我們的開發效率的

寫一個簡單的虛擬列表

效果圖

這裡筆者直接上程式碼,大家複製貼上即可使用,筆者寫了一些註釋,以便於大家理解。當然也可以去筆者的倉庫中去瞅瞅哦,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>
      &nbsp;&nbsp;<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>

使用vxetable外掛實現虛擬列表

如果不是列表,是table表格的話,筆者這裡推薦一個好用的UI元件,vxetable,看名字就知道做的是表格相關的業務。其中就包括虛擬列表。

vue2vue3版本都支援,效能比較好,官方說:虛擬滾動(最大可以支撐 5w 列、30w 行)

強大!

官方網站地址:https://vxetable.cn/v3/#/tabl...

效果圖

效果很絲滑

安裝使用程式碼

注意安裝版本,筆者使用的版本如下:

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另外開啟一個執行緒去操作程式碼邏輯,收益並不是特別大(假如使用虛擬滾動列表外掛的情況下)

不過也算是一個擴充的思路吧,面試的時候,倒是可以說一說,提一提。

Web Worker不熟悉的道友們,可以看看筆者之前的這篇文章:《效能最佳化之使用vue-worker外掛(基於Web Worker)開啟多執行緒運算提高效率

方案八 未雨綢繆,防患於未然

以下為筆者愚見,僅供參考...

  • 在上述解決方案都說完以後,並沒有結束。
  • 實際上本題目在考查候選人知識的廣度和深度以外,更是考查了候選人的處理問題的思考方式,這一點尤其重要!
  • 筆者曾做過候選人去求職,也曾做過面試官去面試。就程式設計師開發工作而言,技術知識點不熟悉,可以快速學習,如文件、谷歌、百度、技術交流群,相關同事都可提供一定的支援
  • 更重要的是看中候選人的思考方式,思維模式
  • 試想,兩個候選人實力水平差不多,但是一個只知道埋頭苦幹,有活就幹,不去斟酌;而另外一個卻是在用心工作的時候,也會仰望星空,會分析如何幹活能夠高價效比地完成任務,注重過程與結果
  • 這樣的話,哪個更加受歡迎一些呢?

如果筆者是候選人,筆者在說了上述7種方案以後,會再補充第八種方案:未雨綢繆,防患於未然


場景模擬

面試官隨意打量著其手中我的簡歷,撫須怪叫一聲:“小子,後端要一次性返回10萬條資料給你,你如何處理?”

我眉毛一挑,歪嘴一笑:“在上述7種方案陳述完以後,我想類似的問題,我們可以從根本上去解決。即第八種方案,要未雨綢繆,防患於未然。”

“哦?”面試官心中疑惑,緩緩放下我的簡歷:“願聞其詳。”

我不緊不慢地答道:“在具體開發工作中,我們在接到一個需求時,在技術評審期間,我們就要和後端去商量比較合適的技術解決方案。這個問題是後端要一次性返回我10萬條資料,重點並不在10萬條這麼多資料,而在於後端為什麼要這樣做?”

面試官抬頭,認真聽了起來。

我一字一頓地說道:“除去業務真正需要這種方案的話,後端這樣做的原因大致有兩種,第一種他不太懂sql的limit語句,但這基本不可能,第二種就是他有事情,隨便敷衍寫了一下。所以,就是要和他溝通,從大資料量介面請求時長過長,以及過多的dom元素渲染導致效能變差,以及專案的可維護性等角度去溝通,我相信只要正確的溝通,就能從根源上去避免這種不太合理的情況發生。”

面試官又突然狡黠地發問:“要是溝通以後,後端死活不給你分頁呢?你咋辦?你的溝通無效果!你如何處理!人家不聽你的!”似乎是覺得這個問題很刁鑽,他雙臂抱在胸前,靠在椅背上,等待著我臉上即將綻放的的回答不上來地尷尬笑容。

我內心冷哼一聲:雕蟲小技...

我盯著面試官的眼睛,認真說道:“如果工作中溝通無效果,要麼是我自己溝通語言表達的問題,這一點我會注意,不斷提升自己的溝通技巧和說話方式,要麼就是...”

我聲音揚起了三分:“我溝通的這個人有問題!他工作摸魚偷懶耍滑!固執己見!為難他人!高高在上!自以為是!這種情況下,我會找到我的直屬領導去介入,因為這已經不是專案的需求問題了,而是員工的基本素養問題!”

停頓了一秒,我聲音又柔和了幾分:“但是,但是我相信我們們公司員工中是絕對沒有這樣的人存在的,各個都是能力強悍,態度端正的優秀員工。畢竟我們們公司在行業中久負盛名,我也是因此慕名而來的。您說對吧?”

面試官眼中閃過震驚之色,他沒有想到我居然把皮球又踢給他了,不過他為了維持形象,旋即恢復了鎮定,只是面部肌肉在止不住的微微顫抖。

我又補充道:“實際上在工作中,前端作為比較貼近使用者的角色而言,需要和各個崗位的同事進行溝通,比如後端、產品、UI、測試等。我們需要透過合理的溝通方式,去提升工作效率,完成專案,實現自己的價值,為公司創造收益,我想這是每一個員工需要做的,也是必須要做到的。”

面試官又撫須怪叫一聲:“小子表現還行,你被錄用了!一個月工資2200,自帶電腦,無社無金,007工作制,不能偷吃公司零食,以及...”

我:阿噠...

總結

有效的溝通,源自於解決問題的思維模式,在多數情況下,重要性,大於當下所掌握的技術知識點

如果覺得文章幫到了您,歡迎不吝star哦 ^_^

相關文章