扁平樹狀資料處理及多層關鍵字搜尋實現

viyam發表於2019-07-03

一、問題背景及需求

1.需求

  • 實現多層關鍵字模糊搜尋;
  • 搜尋整顆樹節點,包括非子節點和子節點,命中時自動展開其父節點;
  • 非子節點命中時,其子節點全部保留;
  • 節點命中關鍵字高亮。

[搜尋前樹狀展示]

扁平樹狀資料處理及多層關鍵字搜尋實現

[搜尋關鍵字"測試"]

扁平樹狀資料處理及多層關鍵字搜尋實現

2.存在問題

  • 後端返回的資料為扁平化資料,展示需要轉成樹狀結構。
  • 資料量大,共4000+資料,為效能考慮,實現搜尋功能時需要減少遍歷次數。

扁平樹狀資料處理及多層關鍵字搜尋實現

二、實現原理

1.扁平資料轉化樹狀結構實現

原理

  • 將散亂扁平化的節點構造成以id為索引的物件據

扁平樹狀資料處理及多層關鍵字搜尋實現

  • 遍歷扁平化節點,逐一尋找其父節點,並插入。

    同時判斷是否根節點,若是直接抽出根節點。

扁平樹狀資料處理及多層關鍵字搜尋實現

程式碼實現

需要遍歷兩次資料:構造以id為索引的物件、尋找父節點據

(根節點parent === '0')

/**
 * 扁平結構轉成樹狀結構
 * @param category 扁平資料
 */
formateTree = (category) => {
  const treeMap = {} // 以id為索引的物件
  const tree = [] // 最後樹狀結果
  
  category.forEach((o) => {
    treeMap[o.id] = Object.assign(o, { children: [] }
  })
  
  category.forEach((o) => {
    if (o.parent !== '0') {
      treeMap[o.parent].children.push(treeMap[o.id])
    } else if (o.parent === '0') {
      tree.push(treeMap[o.id])
    }
  })
  return tree
}
複製程式碼

2.實現多層關鍵字模糊搜尋

準備

  • 樹狀結構
  • 以id為索引的物件
  • 存放各路徑下第一個命中節點的陣列 filterNode[]
  • 存放最終結果的陣列 treeData[]

原理

1.【遞迴】根據關鍵字遍歷樹狀結構尋找各路徑下第一命中節點 findTargetNode() ,並插入filterNode[],將節點parent 插入expandedList[]

命中標記:⭐

扁平樹狀資料處理及多層關鍵字搜尋實現

2.【遞迴】展開非子節點下包含關鍵位元組點的路徑 findExpandedNode() ,將命中節點parent 插入expandedLish[]

⚠️ 注意,如中間有兩層以上沒有命中關鍵字,則無法展開,最後需要根據expandedList[] 中的節點向上補齊 insertNodeParent()

扁平樹狀資料處理及多層關鍵字搜尋實現

3.【遞迴】尋找命中節點的父節點,拼接成樹 findParend()

扁平樹狀資料處理及多層關鍵字搜尋實現

4.expandedList[] 去重。

【遞迴】根據filterNode[] 中的節點向上補齊已展開節點的各級父節點,用到索引物件 insertNodeParent()

5.原理同3

6.expandedList[] 去重。

程式碼實現

  • findTargetNode()

    第一命中節點為末節點時:插入(將該節點push 進filterNode[] ),展開(並將其父id push 到expandedList[] )。

    第一命中節點為非子節點時:插入,並需要遍歷該路徑繼續尋找剩餘命中節點 findExpandedNode()

const findTargetNode = (val, tree, filterNode, expandedList) => {
  tree.forEach((item) => {
    if (item.title.indexOf(val) > -1) {
      if (!item.children.length) {
        filterNode.push(item);
        expandedList.push(item.parent);
      } else {
        filterNode.push(item);
        expandedList.push(item.parent);
        findExpandedNode(val, tree, expandedList);
      }
    } else {
      findTargetNode(val, item.children, filterNode, expandedList);
    }
  })
}
複製程式碼

  • findExpandedNode()

    末節點包含關鍵字:展開;不包含:不處理。

    非末節點包含關鍵字:展開,繼續遞迴;不包含:繼續遞迴。

const findExpandedNode = (val, tree, expandedList) => {
  tree.forEach((item) => {
    if (!item.children.length && item.title.indexOf(val) > -1) {
      expandedList.push(item.parent);
    } else if (item.children.length && item.title.indexOf(val) > -1) { 
      expandedList.push(item.parent);
      findExpandedNode(val, item.children, expandedList)
    } else if (item.children.length && item.title.indexOf(val) === -1) {
      findExpandedNode(val, item.children, expandedList)
    }
  })
}
複製程式碼

  • findParend()

    父節點不是根節點

      目前物件樹中父節點是否已包含該節點,包含:不梳理;未包含:插入父節點。(避免重複)
    複製程式碼

    父節點為根節點

      目前結果中,是否已包含該根節點,包含:不處理;未包含:插入結果中。 
    複製程式碼
const findParend = (item, treeMap, treeData, expandedList) => {
  if (item.parent !== '0') {
    const ids = treeMap[item.parent].children.map(o => o.id);
    if (!ids.includes(item.id)) {
      treeMap[item.parent].children.push(item);
      expandedList.push(item.parent);
      findParend(treeMap[item.parent], treeMap, treeData, expandedList);
    }
  } else {
    const ids = treeData.map(o => o.id);
    if (!ids.includes(item.id)) {
      treeData.push(item);
      expandedList.push(item.id)
    }
  }
}
複製程式碼

  • expandedList[] 去重

    Es6 set資料結構

expandedList = [...new Set(expandedList)]
複製程式碼

  • insertNodeParent()

    當前節點為非根節點時,插入該節點父id

    當前節點為根節點時,返回

const insertNodeParent = (key, expandedList, treeMap) => {
  if (key === '0') {
    return 
  }
  const { parent } = treeMap[key]
  expandedList.push(parent)
  if (parent !== '0') {
    insertNodeParent(parent, expandedList, treeMap)
  }
}
複製程式碼

相關文章