爲了解析後端數據,我竟然寫了個遞歸?

代碼倉庫:https://github.com/Haixiang6123/tree-parser

曾經的我特別討厭 LeetCode 算法題,當時就覺得寫項目好玩,算法沒什麼用。不喜歡歸不喜歡,爲了面試,還是寫了 476 道題 = =。非常感激默默地刷題的那段時光,在處理數據方面確實給了我不一樣的思路。算法和數據結構果然還是基本功呀。

需求

我接到的需求很簡單:後端返回一個 JSON,頁面展示多個下拉選擇器,根據用戶不同的選擇篩選不同的數據。例如:

而後端給我們的數據是這樣的:

const data = {
  '2020-10-10': {
    success: {
      text: [
        {name: '張三', content: '你好'},
        {name: '李四', content: '哈哈哈哈'},
        {name: '王五', content: 'EZEZ'},
      ],
      audio: [
        {name: '小明', content: '喂喂喂'},
        {name: '小紅', content: 'Hello'},
      ]
    },
    fail: {
      text: [
        {name: '張三', content: 'Yoyoyo'},
        {name: '李四', content: 'yeyeye'},
        {name: '王五', content: 'rerere'},
      ],
      audio: [
        {name: '小明', content: '失敗了哦'},
        {name: '小紅', content: '你好呀'},
      ]
    },
    sending: {
      text: [
        {name: '張三', content: '正在發送'},
        {name: '李四', content: '發送着的文字'},
      ],
      audio: [
        {name: '小明', content: '在弄了'},
        {name: '小紅', content: '很簡單'},
      ]
    }
  },
  '2020-10-11': {
   ...
  }
  ...
}

乍一看,感覺這個結構還是很清晰的,時間,消息狀態,消息類型,再到消息內容都以樹狀結構返回。但是,對應到我們的需求,就感覺有點不對勁。

問題

首先,我們要展示可選項,按這裏的需求就是時間、消息狀態、消息類型。那就要對應3個數組:

const dateOptions = ['2020-10-10', '2020-10-11']
const statusOptions = ['success', 'fail', 'sending']
const typeOptions = ['text', 'audio']

剛開始可能會想用 Object.keys() 去拿,但是如果我們不斷地循環,循環再循環把這些拼起來就會現好麻煩啊。

第二個問題我們要面對的就是怎麼去獲取選中結果的過濾結果。假如選中 2020-10-10 就要在這個對象裏將數組裏的內容都拼在一起,以此類推。

其實這兩個問題抽象出來就是怎麼在一棵樹裏收集結果,那都簡單的方法就是 DFS 或者 BFS 去找。

實現

收集所有結果

最好的效果就是丟什麼對象進去都可以直接返回那個對象下所有數組的合集,例如:

// 返回所有數據
collectArraysDFS(data)
// 返回 2020-10-10 下面的數據
collectArraysDFS(data['2020-10-10'])
// 返回 2020-10-10 且成功的數據
collectArraysDFS(data['2020-10-10']['success'])
...

這個問題還算比較簡單,使用 DFS 是比較好做的,只要判斷當前是否爲 Array,如果是 Array,則加入結果,否則如果是 Object,則進入下一步的遞歸。

const collectArraysDFS = (object) => {
  if (!object) { return [] }

  // 如果本身就是數組,直接返回
  if (object instanceof Array) { return object }

  return Object.values(object).reduce((prev, value) => {
    // 繼續遞歸
    if (value instanceof Object) {
      prev = prev.concat(collectArraysDFS(value));
    }

    return prev;
  }, []);
};

BFS 實現的版本:

const collectArraysBFS = (object) => {
  if (!object) { return [] }

  let queue = [object];
  let result = [];

  while (queue.length > 0) {
    const curtNode = queue.pop();

    // 如果是數組,則存起來
    if (curtNode instanceof Array) {
      result = result.concat(curtNode);
    }

    // 如果還是對象,則繼續下一層
    if (curtNode instanceof Object) {
      const values = Object.values(curtNode);
      queue = queue.concat(values);
    }
  }

  return result;
};

收集所有選項

我們希望的效果是,給一個對象,我要哪一層的 key,就返回哪一層的 key,如:

// 返回第2層的所有的key
collectKeysDFS(data, 2, 1)

這裏的思路是 DFS 走完整個樹,然後設定好一個 targetLevel,表示只會收集那一層的所有 keys 就好,同時我們還需要 step 來計算當前層。只要在到了 targetLevel,就 Object.keys() 一下,表返回結果,在前面的層則負責收集結果就好了。最後回到 root,就能收集到所有的 key。

簡單的 DFS 實現如下:

const collectKeysDFS = (object, targetLevel, step) => {
  if (!object || targetLevel < step) { return [] }

  // 到達層數,返回所有 keys
  if (step === targetLevel) {
    return Object.keys(object);
  }

  // 繼續遞歸
  return Object.values(object).reduce((prev, value) => {
    if (value instanceof Object) {
      return prev.concat(collectKeysDFS(value, targetLevel, step + 1));
    }
  }, []);
};

使用 BFS 的版本:

const collectArraysBFS = (object) => {
  if (!object) { return [] }

  let queue = [object];
  let result = [];

  while (queue.length > 0) {
    const curtNode = queue.pop();

    // 如果是數組,則存起來
    if (curtNode instanceof Array) {
      result = result.concat(curtNode);
    }

    // 如果還是對象,則繼續下一層
    if (curtNode instanceof Object) {
      const values = Object.values(curtNode);
      queue = queue.concat(values);
    }
  }

  return result;
};

最後

看到這,可能會有人說:這種代碼以後會很難維護,不好看懂,過度優化等等等等。

這裏只是提供另一種思路。想想如果都是一層一層 for loop 來解析數據,那業務代碼就會變得特別多且冗餘,而這種比較抽象的工具函數卻可以應對下一次相同數據結構。

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章