效果:
使用原生 JS 实现一个简单的树插件。
首先元数据的数据结构,每个节点需要包含自己的主键和父级节点的主键,才能描述树形结构。比如:
[{ "id": "1", "name": "一级目录1", "fatherId": "0", }, { "id": "2", "name": "二级目录1", "fatherId": "1" }, { "id": "3", "name": "二级目录2", "fatherId": "1" }, { "id": "4", "name": "三级目录1", "fatherId": "2" }, { "id": "5", "name": "三级目录2", "fatherId": "2" }, { "id": "6", "name": "一级目录2", "fatherId": "0" }, { "id": "7", "name": "三极目录3", "fatherId": "3" }, { "id": "8", "name": "四级目录1", "fatherId": "7" }, { "id": "9", "name": "五级目录1", "fatherId": "8" } ]
处理该类数据,处理为树形数据结构,方便渲染:
// 将数组初始化为树结构 initTreeData: function (arr) { for (let i = 0; i < arr.length; i++) this.nodesMap[arr[i].id] = arr[i]; let reArr = []; for (let i = 0; i < arr.length; i++) { arr[i]['showChilds'] = true; if (!this.nodesMap[arr[i].fatherId]) reArr.push(arr[i]); else { let fatherNode = this.nodesMap[arr[i].fatherId]; fatherNode.childs = fatherNode.childs || []; fatherNode.childs.push(arr[i]); } } return reArr; },
处理后的数据结构如下:
[ { "id": "1", "name": "一级目录1", "fatherId": "0", "showChilds": true, "childs": [ { "id": "2", "name": "二级目录1", "fatherId": "1", "showChilds": true, "childs": [ { "id": "4", "name": "三级目录1", "fatherId": "2", "showChilds": true }, { "id": "5", "name": "三级目录2", "fatherId": "2", "showChilds": true } ] }, { "id": "3", "name": "二级目录2", "fatherId": "1", "showChilds": true, "childs": [ { "id": "7", "name": "三极目录3", "fatherId": "3", "showChilds": true, "childs": [ { "id": "8", "name": "四级目录1", "fatherId": "7", "showChilds": true, "childs": [ { "id": "9", "name": "五级目录1", "fatherId": "8", "showChilds": true } ] } ] } ] } ] }, { "id": "6", "name": "一级目录2", "fatherId": "0", "showChilds": true } ]
将数据渲染到页面上:
// 根据数组渲染树 createDom: function (data) { let self = this; let fatherDom = document.getElementById(`${this.rootId}-my-tree-${data.fatherId}`); // 虚拟根节点 if (!fatherDom) fatherDom = document.getElementById(this.rootId); let dom = document.createElement("div"); dom.id = `${this.rootId}-my-tree-${data.id}`; dom.className = 'treeItem'; let iconDom = document.createElement("div"); iconDom.id = `${this.rootId}-my-tree-icon-${data.id}`; iconDom.className = "itemIcon"; iconDom.innerHTML = "-"; // if (data.showChilds) iconDom.innerHTML = "-"; // else iconDom.innerHTML = "+"; dom.appendChild(iconDom); dom.innerHTML += data.name; fatherDom.appendChild(dom); iconDom = document.getElementById(`${this.rootId}-my-tree-icon-${data.id}`); iconDom.onclick = function () { self.iconClickHandler(iconDom); }; // 递归渲染子树 if (data.childs && data.childs.length > 0) { for (let i = 0; i < data.childs.length; i++) this.createDom(data.childs[i]); } },
折叠、展开子树:
iconClickHandler(dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; if (node.showChilds) { this.hideHandler(dom); dom.innerHTML = "+"; } else { this.showHandler(dom); dom.innerHTML = "-"; } }, // 隐藏子树 hideHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = false; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'none'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); this.hideHandler(childIconDom); } }, // 渲染子树 showHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = true; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'block'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); let childNode = this.nodesMap[childs[i].id]; childIconDom.innerHTML = "+"; // 递归渲染子树 this.hideHandler(childIconDom); } }
完整组件代码:
function MyTree(list, rootId) { // 简单的深拷贝 this.list = JSON.parse(JSON.stringify(list)); this.rootId = rootId; this.nodesMap = {}; this.treeData = []; } MyTree.prototype = { init: function () { this.treeData = this.initTreeData(this.list); for (let i = 0; i < this.treeData.length; i++) { this.createDom(this.treeData[i]); } }, // 将数组初始化为树结构 initTreeData: function (arr) { for (let i = 0; i < arr.length; i++) this.nodesMap[arr[i].id] = arr[i]; let reArr = []; for (let i = 0; i < arr.length; i++) { arr[i]['showChilds'] = true; if (!this.nodesMap[arr[i].fatherId]) reArr.push(arr[i]); else { let fatherNode = this.nodesMap[arr[i].fatherId]; fatherNode.childs = fatherNode.childs || []; fatherNode.childs.push(arr[i]); } } return reArr; }, // 根据数组渲染树 createDom: function (data) { let self = this; let fatherDom = document.getElementById(`${this.rootId}-my-tree-${data.fatherId}`); // 虚拟根节点 if (!fatherDom) fatherDom = document.getElementById(this.rootId); let dom = document.createElement("div"); dom.id = `${this.rootId}-my-tree-${data.id}`; dom.className = 'treeItem'; let iconDom = document.createElement("div"); iconDom.id = `${this.rootId}-my-tree-icon-${data.id}`; iconDom.className = "itemIcon"; iconDom.innerHTML = "-"; // if (data.showChilds) iconDom.innerHTML = "-"; // else iconDom.innerHTML = "+"; dom.appendChild(iconDom); dom.innerHTML += data.name; fatherDom.appendChild(dom); iconDom = document.getElementById(`${this.rootId}-my-tree-icon-${data.id}`); iconDom.onclick = function () { self.iconClickHandler(iconDom); }; // 递归渲染子树 if (data.childs && data.childs.length > 0) { for (let i = 0; i < data.childs.length; i++) this.createDom(data.childs[i]); } }, iconClickHandler(dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; if (node.showChilds) { this.hideHandler(dom); dom.innerHTML = "+"; } else { this.showHandler(dom); dom.innerHTML = "-"; } }, // 隐藏子树 hideHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = false; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'none'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); this.hideHandler(childIconDom); } }, // 渲染子树 showHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = true; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'block'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); let childNode = this.nodesMap[childs[i].id]; childIconDom.innerHTML = "+"; // 递归渲染子树 this.hideHandler(childIconDom); } } }
测试 html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>MyTree</title> <style> .treeItem { padding-left: 20px; margin: 16px; } .itemIcon { display: inline-block; width: 18px; height: 18px; line-height: 16px; color: white; font-weight: bolder; text-align: center; font-size: 16px; border-radius: 50%; background: rgb(85, 141, 213); margin-right: 10px; cursor: pointer; } </style> </head> <body> <div style="30%;border:0.1px;margin:1%;display: inline-block;position: absolute;left: 0px;"> <!-- 虚拟根节点 --> <div id="my-tree-root0" class="treeItem">虚拟根节点0</div> </div> <div style="30%;border:0.1px;margin:1%;display: inline-block;height:100vh;position: absolute;left: 30%;"> <!-- 虚拟根节点 --> <div id="my-tree-root1" class="treeItem">虚拟根节点1</div> </div> <div style="30%;border:0.1px;margin:1%;display: inline-block;position: absolute;left: 60%;"> <!-- 虚拟根节点 --> <div id="my-tree-root2" class="treeItem">虚拟根节点2</div> </div> <script> var list = [{ "id": "1", "name": "一级目录1", "fatherId": "0", }, { "id": "2", "name": "二级目录1", "fatherId": "1" }, { "id": "3", "name": "二级目录2", "fatherId": "1" }, { "id": "4", "name": "三级目录1", "fatherId": "2" }, { "id": "5", "name": "三级目录2", "fatherId": "2" }, { "id": "6", "name": "一级目录2", "fatherId": "0" }, { "id": "7", "name": "三极目录3", "fatherId": "3" }, { "id": "8", "name": "四级目录1", "fatherId": "7" }, { "id": "9", "name": "五级目录1", "fatherId": "8" } ]; function MyTree(list, rootId) { // 简单的深拷贝 this.list = JSON.parse(JSON.stringify(list)); this.rootId = rootId; this.nodesMap = {}; this.treeData = []; } MyTree.prototype = { init: function () { this.treeData = this.initTreeData(this.list); for (let i = 0; i < this.treeData.length; i++) { this.createDom(this.treeData[i]); } }, // 将数组初始化为树结构 initTreeData: function (arr) { for (let i = 0; i < arr.length; i++) this.nodesMap[arr[i].id] = arr[i]; let reArr = []; for (let i = 0; i < arr.length; i++) { arr[i]['showChilds'] = true; if (!this.nodesMap[arr[i].fatherId]) reArr.push(arr[i]); else { let fatherNode = this.nodesMap[arr[i].fatherId]; fatherNode.childs = fatherNode.childs || []; fatherNode.childs.push(arr[i]); } } return reArr; }, // 根据数组渲染树 createDom: function (data) { let self = this; let fatherDom = document.getElementById(`${this.rootId}-my-tree-${data.fatherId}`); // 虚拟根节点 if (!fatherDom) fatherDom = document.getElementById(this.rootId); let dom = document.createElement("div"); dom.id = `${this.rootId}-my-tree-${data.id}`; dom.className = 'treeItem'; let iconDom = document.createElement("div"); iconDom.id = `${this.rootId}-my-tree-icon-${data.id}`; iconDom.className = "itemIcon"; iconDom.innerHTML = "-"; // if (data.showChilds) iconDom.innerHTML = "-"; // else iconDom.innerHTML = "+"; dom.appendChild(iconDom); dom.innerHTML += data.name; fatherDom.appendChild(dom); iconDom = document.getElementById(`${this.rootId}-my-tree-icon-${data.id}`); iconDom.onclick = function () { self.iconClickHandler(iconDom); }; // 递归渲染子树 if (data.childs && data.childs.length > 0) { for (let i = 0; i < data.childs.length; i++) this.createDom(data.childs[i]); } }, iconClickHandler(dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; if (node.showChilds) { this.hideHandler(dom); dom.innerHTML = "+"; } else { this.showHandler(dom); dom.innerHTML = "-"; } }, // 隐藏子树 hideHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = false; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'none'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); this.hideHandler(childIconDom); } }, // 渲染子树 showHandler: function (dom) { if (!dom || !dom.id) return; let id = dom.id; if (id.indexOf("my-tree-icon-") == -1) return; id = id.substring(id.indexOf("my-tree-icon-") + 13); let node = this.nodesMap[id]; if (!node) return; let childs = node.childs; if (!childs) return; node.showChilds = true; for (let i = 0; i < childs.length; i++) { let childDom = document.getElementById(`${this.rootId}-my-tree-${childs[i].id}`); if (!childDom) continue; childDom.style.display = 'block'; let childIconDom = document.getElementById(`${this.rootId}-my-tree-icon-${childs[i].id}`); let childNode = this.nodesMap[childs[i].id]; childIconDom.innerHTML = "+"; // 递归渲染子树 this.hideHandler(childIconDom); } } } var myTree0 = new MyTree(list, "my-tree-root0"); myTree0.init(); var myTree1 = new MyTree(list, "my-tree-root1"); myTree1.init(); var myTree2 = new MyTree(list, "my-tree-root2"); myTree2.init(); </script> </body> </html>