一、問題背景及需求
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)
}
}
複製程式碼